mirror of
https://github.com/theniceboy/.config.git
synced 2026-07-16 22:01:21 +08:00
pinchtab mcp
This commit is contained in:
parent
57e816f512
commit
b4a7d0ae86
3 changed files with 359 additions and 3 deletions
283
opencode/tool/pinchtab-mcp.mjs
Normal file
283
opencode/tool/pinchtab-mcp.mjs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { z } from "zod"
|
||||
|
||||
const baseUrl = (process.env.PINCHTAB_URL || "http://127.0.0.1:9867").replace(/\/+$/, "")
|
||||
const token = process.env.PINCHTAB_TOKEN || ""
|
||||
|
||||
const server = new McpServer({
|
||||
name: "pinchtab",
|
||||
version: "0.1.0",
|
||||
})
|
||||
|
||||
function toolResult(data) {
|
||||
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2)
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
}
|
||||
}
|
||||
|
||||
function toolError(error) {
|
||||
const text = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
function withDefinedValues(input) {
|
||||
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined))
|
||||
}
|
||||
|
||||
async function requestPinchtab(path, { method = "GET", query, body } = {}) {
|
||||
const url = new URL(`${baseUrl}${path}`)
|
||||
for (const [key, value] of Object.entries(query || {})) {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
const headers = {}
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
if (body !== undefined) {
|
||||
headers["Content-Type"] = "application/json"
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
const contentType = response.headers.get("content-type") || ""
|
||||
const text = await response.text()
|
||||
|
||||
let data = text
|
||||
if (contentType.includes("application/json")) {
|
||||
try {
|
||||
data = text ? JSON.parse(text) : {}
|
||||
} catch {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = typeof data === "string" ? data : JSON.stringify(data)
|
||||
throw new Error(`${response.status} ${response.statusText}: ${message}`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_health",
|
||||
{
|
||||
description: "Check Pinchtab server status",
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const result = await requestPinchtab("/health")
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_tabs",
|
||||
{
|
||||
description: "List open browser tabs",
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const result = await requestPinchtab("/tabs")
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_navigate",
|
||||
{
|
||||
description: "Navigate a tab to a URL",
|
||||
inputSchema: {
|
||||
url: z.string().url(),
|
||||
tabId: z.string().optional(),
|
||||
newTab: z.boolean().optional(),
|
||||
},
|
||||
},
|
||||
async ({ url, tabId, newTab }) => {
|
||||
try {
|
||||
const result = await requestPinchtab("/navigate", {
|
||||
method: "POST",
|
||||
body: withDefinedValues({ url, tabId, newTab }),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_snapshot",
|
||||
{
|
||||
description: "Get accessibility snapshot (JSON or text)",
|
||||
inputSchema: {
|
||||
tabId: z.string().optional(),
|
||||
filter: z.enum(["interactive"]).optional(),
|
||||
diff: z.boolean().optional(),
|
||||
depth: z.number().int().optional(),
|
||||
format: z.enum(["json", "text"]).optional(),
|
||||
},
|
||||
},
|
||||
async ({ tabId, filter, diff, depth, format }) => {
|
||||
try {
|
||||
const result = await requestPinchtab("/snapshot", {
|
||||
query: withDefinedValues({
|
||||
tabId,
|
||||
filter,
|
||||
diff: diff ? "true" : undefined,
|
||||
depth,
|
||||
format: format === "text" ? "text" : undefined,
|
||||
}),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_text",
|
||||
{
|
||||
description: "Extract readable text from page",
|
||||
inputSchema: {
|
||||
tabId: z.string().optional(),
|
||||
mode: z.enum(["clean", "raw"]).optional(),
|
||||
},
|
||||
},
|
||||
async ({ tabId, mode }) => {
|
||||
try {
|
||||
const result = await requestPinchtab("/text", {
|
||||
query: withDefinedValues({
|
||||
tabId,
|
||||
mode: mode === "raw" ? "raw" : undefined,
|
||||
}),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_action",
|
||||
{
|
||||
description: "Run a single browser action",
|
||||
inputSchema: {
|
||||
tabId: z.string().optional(),
|
||||
kind: z.enum(["click", "type", "fill", "press", "focus", "hover", "select", "scroll"]),
|
||||
ref: z.string().optional(),
|
||||
selector: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
value: z.string().optional(),
|
||||
nodeId: z.number().int().optional(),
|
||||
scrollX: z.number().int().optional(),
|
||||
scrollY: z.number().int().optional(),
|
||||
waitNav: z.boolean().optional(),
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const result = await requestPinchtab("/action", {
|
||||
method: "POST",
|
||||
body: withDefinedValues(args),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_evaluate",
|
||||
{
|
||||
description: "Evaluate JavaScript expression in page context",
|
||||
inputSchema: {
|
||||
expression: z.string().min(1),
|
||||
tabId: z.string().optional(),
|
||||
},
|
||||
},
|
||||
async ({ expression, tabId }) => {
|
||||
try {
|
||||
const result = await requestPinchtab("/evaluate", {
|
||||
method: "POST",
|
||||
body: withDefinedValues({ expression, tabId }),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_tab",
|
||||
{
|
||||
description: "Open or close tabs",
|
||||
inputSchema: {
|
||||
action: z.enum(["new", "close"]),
|
||||
tabId: z.string().optional(),
|
||||
url: z.string().url().optional(),
|
||||
},
|
||||
},
|
||||
async ({ action, tabId, url }) => {
|
||||
try {
|
||||
if (action === "close" && !tabId) {
|
||||
return toolError("tabId is required when action is close")
|
||||
}
|
||||
|
||||
const result = await requestPinchtab("/tab", {
|
||||
method: "POST",
|
||||
body: withDefinedValues({ action, tabId, url }),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"pinchtab_screenshot",
|
||||
{
|
||||
description: "Capture JPEG screenshot as base64",
|
||||
inputSchema: {
|
||||
tabId: z.string().optional(),
|
||||
quality: z.number().int().min(1).max(100).optional(),
|
||||
},
|
||||
},
|
||||
async ({ tabId, quality }) => {
|
||||
try {
|
||||
const result = await requestPinchtab("/screenshot", {
|
||||
query: withDefinedValues({ tabId, quality }),
|
||||
})
|
||||
return toolResult(result)
|
||||
} catch (error) {
|
||||
return toolError(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const transport = new StdioServerTransport()
|
||||
await server.connect(transport)
|
||||
Loading…
Add table
Add a link
Reference in a new issue