@stablekernel/opencode-cursor 0.8.0 → 0.9.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +110 -11
- package/dist/{chunk-YIEC27VB.js → chunk-HWSH5L4H.js} +24 -5
- package/dist/chunk-HWSH5L4H.js.map +1 -0
- package/dist/plugin/index.js +881 -25
- package/dist/plugin/index.js.map +1 -1
- package/dist/provider/index.js +1 -1
- package/dist/sidecar/agent-host.d.ts +32 -8
- package/dist/sidecar/agent-host.js +16 -1
- package/dist/sidecar/agent-host.js.map +1 -1
- package/dist/sidecar/plugin-tools-mcp.d.ts +189 -0
- package/dist/sidecar/plugin-tools-mcp.js +163 -0
- package/dist/sidecar/plugin-tools-mcp.js.map +1 -0
- package/package.json +7 -7
- package/dist/chunk-YIEC27VB.js.map +0 -1
package/dist/provider/index.js
CHANGED
|
@@ -28,7 +28,12 @@ function serializeError(err) {
|
|
|
28
28
|
const out = { name: err.name, message: err.message };
|
|
29
29
|
for (const k of ["status", "code", "isRetryable", "helpUrl"]) {
|
|
30
30
|
const v = err[k];
|
|
31
|
-
if (
|
|
31
|
+
if (
|
|
32
|
+
typeof v === "number" ||
|
|
33
|
+
typeof v === "string" ||
|
|
34
|
+
typeof v === "boolean"
|
|
35
|
+
)
|
|
36
|
+
out[k] = v;
|
|
32
37
|
}
|
|
33
38
|
return out;
|
|
34
39
|
}
|
|
@@ -43,16 +48,23 @@ function write(payload) {
|
|
|
43
48
|
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
44
49
|
|
|
45
50
|
// `@cursor/sdk`'s bundled local-exec runtime writes its rules/skills
|
|
46
|
-
// load-completion diagnostics straight to `console.log
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// process.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
51
|
+
// load-completion diagnostics straight to `console.log`, and its shell-parser
|
|
52
|
+
// emits a one-shot "tree-sitter natives unavailable" diagnostic via
|
|
53
|
+
// `console.warn` (no public logger hook exists to redirect either — see
|
|
54
|
+
// src/provider/cursor-log-intercept.ts, which applies the identical pattern
|
|
55
|
+
// for the in-process transport). This process's own JSONL protocol never uses
|
|
56
|
+
// console.log/console.warn (only process.stdout.write via write() above), so
|
|
57
|
+
// they are entirely free for the SDK's use: recognized lines are forwarded
|
|
58
|
+
// to the parent as a structured "log" event instead of being written as raw,
|
|
59
|
+
// unparseable text.
|
|
53
60
|
const RULE_LOAD_PATTERN =
|
|
54
61
|
/^\d{2}:\d{2}:\d{2}\.\d{3}\s+INFO\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\s+ctx=\S+)?\s+meta=\{([^}]*)\}\s*$/;
|
|
55
62
|
|
|
63
|
+
// One-shot SDK load diagnostics recognized on console.warn (prefix-matched).
|
|
64
|
+
const SDK_WARNING_PREFIXES = [
|
|
65
|
+
"shell-parser: tree-sitter natives are unavailable in this artifact",
|
|
66
|
+
];
|
|
67
|
+
|
|
56
68
|
function parseLogMeta(raw) {
|
|
57
69
|
const out = {};
|
|
58
70
|
for (const part of raw.split(",")) {
|
|
@@ -82,6 +94,18 @@ console.log = (...args) => {
|
|
|
82
94
|
originalConsoleLog(...args);
|
|
83
95
|
};
|
|
84
96
|
|
|
97
|
+
const originalConsoleWarn = console.warn.bind(console);
|
|
98
|
+
console.warn = (...args) => {
|
|
99
|
+
if (args.length === 1 && typeof args[0] === "string") {
|
|
100
|
+
const line = args[0].replace(ANSI_PATTERN, "");
|
|
101
|
+
if (SDK_WARNING_PREFIXES.some((prefix) => line.startsWith(prefix))) {
|
|
102
|
+
write({ ev: "log", level: "warn", message: line });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
originalConsoleWarn(...args);
|
|
107
|
+
};
|
|
108
|
+
|
|
85
109
|
let sdkPromise;
|
|
86
110
|
function loadSdk() {
|
|
87
111
|
// OPENCODE_CURSOR_SDK_PATH lets tests substitute a fake SDK module.
|
|
@@ -5,7 +5,8 @@ function serializeError(err) {
|
|
|
5
5
|
const out = { name: err.name, message: err.message };
|
|
6
6
|
for (const k of ["status", "code", "isRetryable", "helpUrl"]) {
|
|
7
7
|
const v = err[k];
|
|
8
|
-
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean")
|
|
8
|
+
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean")
|
|
9
|
+
out[k] = v;
|
|
9
10
|
}
|
|
10
11
|
return out;
|
|
11
12
|
}
|
|
@@ -17,6 +18,9 @@ function write(payload) {
|
|
|
17
18
|
}
|
|
18
19
|
var ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
19
20
|
var RULE_LOAD_PATTERN = /^\d{2}:\d{2}:\d{2}\.\d{3}\s+INFO\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\s+ctx=\S+)?\s+meta=\{([^}]*)\}\s*$/;
|
|
21
|
+
var SDK_WARNING_PREFIXES = [
|
|
22
|
+
"shell-parser: tree-sitter natives are unavailable in this artifact"
|
|
23
|
+
];
|
|
20
24
|
function parseLogMeta(raw) {
|
|
21
25
|
const out = {};
|
|
22
26
|
for (const part of raw.split(",")) {
|
|
@@ -44,6 +48,17 @@ console.log = (...args) => {
|
|
|
44
48
|
}
|
|
45
49
|
originalConsoleLog(...args);
|
|
46
50
|
};
|
|
51
|
+
var originalConsoleWarn = console.warn.bind(console);
|
|
52
|
+
console.warn = (...args) => {
|
|
53
|
+
if (args.length === 1 && typeof args[0] === "string") {
|
|
54
|
+
const line = args[0].replace(ANSI_PATTERN, "");
|
|
55
|
+
if (SDK_WARNING_PREFIXES.some((prefix) => line.startsWith(prefix))) {
|
|
56
|
+
write({ ev: "log", level: "warn", message: line });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
originalConsoleWarn(...args);
|
|
61
|
+
};
|
|
47
62
|
var sdkPromise;
|
|
48
63
|
function loadSdk() {
|
|
49
64
|
sdkPromise ??= import(process.env.OPENCODE_CURSOR_SDK_PATH || "@cursor/sdk");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sidecar/agent-host.mjs"],"sourcesContent":["/**\n * Cursor agent sidecar — runs under Node and hosts all `@cursor/sdk` agent\n * traffic on behalf of the provider.\n *\n * Why this exists: opencode executes plugins under Bun, whose `node:http2`\n * client breaks Cursor's streaming connect RPC (NGHTTP2_FRAME_SIZE_ERROR);\n * tool-completion updates are lost and every native tool call dangles. Under\n * Node the same stream works, so when Bun is detected the provider spawns this\n * script with Node and proxies agent calls over a JSON-lines stdio protocol\n * (see sidecar-client.ts for the client side).\n *\n * Protocol (one JSON object per line):\n * request: {id, op: \"ping\"|\"create\"|\"resume\"|\"send\"|\"cancel\"|\"close\", ...}\n * response: {id, ok: true, ...} | {id, ok: false, error: {name, message}}\n * send stream: {id, ev: \"update\", update} ... then exactly one of\n * {id, ev: \"result\", result} | {id, ev: \"error\", error}\n *\n * Kept as plain .mjs so tests can spawn it pre-build; tsup also bundles it to\n * dist/sidecar/agent-host.js for production.\n */\nimport { createInterface } from \"node:readline\";\n\n/** Plain-data error shape that survives JSON; name + classification fields\n * preserved so the Bun side can discriminate (see error-classify.ts). */\nfunction serializeError(err) {\n if (err instanceof Error) {\n const out = { name: err.name, message: err.message };\n for (const k of [\"status\", \"code\", \"isRetryable\", \"helpUrl\"]) {\n const v = err[k];\n if (typeof v === \"number\" || typeof v === \"string\" || typeof v === \"boolean\") out[k] = v;\n }\n return out;\n }\n return { name: \"Error\", message: String(err) };\n}\n\nfunction write(payload) {\n process.stdout.write(`${JSON.stringify(payload)}\\n`);\n}\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_PATTERN = /\\x1b\\[[0-9;]*m/g;\n\n// `@cursor/sdk`'s bundled local-exec runtime writes its rules/skills\n// load-completion diagnostics straight to `console.log` (no public logger\n// hook exists to redirect it — see src/provider/cursor-log-intercept.ts,\n// which applies the identical pattern for the in-process transport). This\n// process's own JSONL protocol never uses console.log (only\n// process.stdout.write via write() above), so console.log here is entirely\n// free for the SDK's use: recognized lines are forwarded to the parent as a\n// structured \"log\" event instead of being written as raw, unparseable text.\nconst RULE_LOAD_PATTERN =\n /^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+INFO\\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\\s+ctx=\\S+)?\\s+meta=\\{([^}]*)\\}\\s*$/;\n\nfunction parseLogMeta(raw) {\n const out = {};\n for (const part of raw.split(\",\")) {\n const [key, value] = part.split(\":\").map((s) => s.trim());\n if (!key || value === undefined) continue;\n const num = Number(value);\n if (Number.isFinite(num)) out[key] = num;\n }\n return out;\n}\n\nconst originalConsoleLog = console.log.bind(console);\nconsole.log = (...args) => {\n if (args.length === 1 && typeof args[0] === \"string\") {\n const match = RULE_LOAD_PATTERN.exec(args[0].replace(ANSI_PATTERN, \"\"));\n if (match) {\n const [, service, meta] = match;\n write({\n ev: \"log\",\n level: \"info\",\n message: `${service} load completed`,\n meta: parseLogMeta(meta ?? \"\"),\n });\n return;\n }\n }\n originalConsoleLog(...args);\n};\n\nlet sdkPromise;\nfunction loadSdk() {\n // OPENCODE_CURSOR_SDK_PATH lets tests substitute a fake SDK module.\n sdkPromise ??= import(process.env.OPENCODE_CURSOR_SDK_PATH || \"@cursor/sdk\");\n return sdkPromise;\n}\n\n/** agentId -> SDKAgent */\nconst agents = new Map();\n/** send request id -> Run (for cancel) */\nconst runs = new Map();\n\nasync function handleRequest(req) {\n const { id, op } = req;\n switch (op) {\n case \"ping\": {\n write({ id, ok: true, pid: process.pid });\n return;\n }\n case \"create\":\n case \"resume\": {\n const { Agent } = await loadSdk();\n const agent =\n op === \"resume\"\n ? await Agent.resume(req.agentId, req.options)\n : await Agent.create(req.options);\n agents.set(agent.agentId, agent);\n write({ id, ok: true, agentId: agent.agentId });\n return;\n }\n case \"send\": {\n const agent = agents.get(req.agentId);\n if (!agent) throw new Error(`unknown agent \"${req.agentId}\"`);\n const sendOptions = {\n ...(req.mode ? { mode: req.mode } : {}),\n ...(req.force ? { local: { force: true } } : {}),\n ...(req.idempotencyKey ? { idempotencyKey: req.idempotencyKey } : {}),\n onDelta: ({ update }) => write({ id, ev: \"update\", update }),\n };\n const run = await agent.send(req.message, sendOptions);\n runs.set(id, run);\n // Acknowledge so the client can hand back a cancellable run handle.\n write({ id, ok: true });\n try {\n const result = await run.wait();\n write({ id, ev: \"result\", result });\n } catch (err) {\n write({ id, ev: \"error\", error: serializeError(err) });\n } finally {\n runs.delete(id);\n }\n return;\n }\n case \"cancel\": {\n const run = runs.get(req.sendId);\n if (run) await run.cancel();\n write({ id, ok: true });\n return;\n }\n case \"close\": {\n const agent = agents.get(req.agentId);\n agents.delete(req.agentId);\n try {\n agent?.close();\n } catch {\n // best effort\n }\n write({ id, ok: true });\n return;\n }\n default:\n throw new Error(`unknown op \"${op}\"`);\n }\n}\n\nconst rl = createInterface({ input: process.stdin });\nrl.on(\"line\", (line) => {\n if (!line.trim()) return;\n let req;\n try {\n req = JSON.parse(line);\n } catch (err) {\n write({ id: null, ok: false, error: serializeError(err) });\n return;\n }\n handleRequest(req).catch((err) => {\n write({ id: req.id, ok: false, error: serializeError(err) });\n });\n});\n\n// Parent gone (stdin closed) -> shut down; never outlive the plugin process.\nrl.on(\"close\", () => {\n process.exit(0);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/sidecar/agent-host.mjs"],"sourcesContent":["/**\n * Cursor agent sidecar — runs under Node and hosts all `@cursor/sdk` agent\n * traffic on behalf of the provider.\n *\n * Why this exists: opencode executes plugins under Bun, whose `node:http2`\n * client breaks Cursor's streaming connect RPC (NGHTTP2_FRAME_SIZE_ERROR);\n * tool-completion updates are lost and every native tool call dangles. Under\n * Node the same stream works, so when Bun is detected the provider spawns this\n * script with Node and proxies agent calls over a JSON-lines stdio protocol\n * (see sidecar-client.ts for the client side).\n *\n * Protocol (one JSON object per line):\n * request: {id, op: \"ping\"|\"create\"|\"resume\"|\"send\"|\"cancel\"|\"close\", ...}\n * response: {id, ok: true, ...} | {id, ok: false, error: {name, message}}\n * send stream: {id, ev: \"update\", update} ... then exactly one of\n * {id, ev: \"result\", result} | {id, ev: \"error\", error}\n *\n * Kept as plain .mjs so tests can spawn it pre-build; tsup also bundles it to\n * dist/sidecar/agent-host.js for production.\n */\nimport { createInterface } from \"node:readline\";\n\n/** Plain-data error shape that survives JSON; name + classification fields\n * preserved so the Bun side can discriminate (see error-classify.ts). */\nfunction serializeError(err) {\n if (err instanceof Error) {\n const out = { name: err.name, message: err.message };\n for (const k of [\"status\", \"code\", \"isRetryable\", \"helpUrl\"]) {\n const v = err[k];\n if (\n typeof v === \"number\" ||\n typeof v === \"string\" ||\n typeof v === \"boolean\"\n )\n out[k] = v;\n }\n return out;\n }\n return { name: \"Error\", message: String(err) };\n}\n\nfunction write(payload) {\n process.stdout.write(`${JSON.stringify(payload)}\\n`);\n}\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_PATTERN = /\\x1b\\[[0-9;]*m/g;\n\n// `@cursor/sdk`'s bundled local-exec runtime writes its rules/skills\n// load-completion diagnostics straight to `console.log`, and its shell-parser\n// emits a one-shot \"tree-sitter natives unavailable\" diagnostic via\n// `console.warn` (no public logger hook exists to redirect either — see\n// src/provider/cursor-log-intercept.ts, which applies the identical pattern\n// for the in-process transport). This process's own JSONL protocol never uses\n// console.log/console.warn (only process.stdout.write via write() above), so\n// they are entirely free for the SDK's use: recognized lines are forwarded\n// to the parent as a structured \"log\" event instead of being written as raw,\n// unparseable text.\nconst RULE_LOAD_PATTERN =\n /^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+INFO\\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\\s+ctx=\\S+)?\\s+meta=\\{([^}]*)\\}\\s*$/;\n\n// One-shot SDK load diagnostics recognized on console.warn (prefix-matched).\nconst SDK_WARNING_PREFIXES = [\n \"shell-parser: tree-sitter natives are unavailable in this artifact\",\n];\n\nfunction parseLogMeta(raw) {\n const out = {};\n for (const part of raw.split(\",\")) {\n const [key, value] = part.split(\":\").map((s) => s.trim());\n if (!key || value === undefined) continue;\n const num = Number(value);\n if (Number.isFinite(num)) out[key] = num;\n }\n return out;\n}\n\nconst originalConsoleLog = console.log.bind(console);\nconsole.log = (...args) => {\n if (args.length === 1 && typeof args[0] === \"string\") {\n const match = RULE_LOAD_PATTERN.exec(args[0].replace(ANSI_PATTERN, \"\"));\n if (match) {\n const [, service, meta] = match;\n write({\n ev: \"log\",\n level: \"info\",\n message: `${service} load completed`,\n meta: parseLogMeta(meta ?? \"\"),\n });\n return;\n }\n }\n originalConsoleLog(...args);\n};\n\nconst originalConsoleWarn = console.warn.bind(console);\nconsole.warn = (...args) => {\n if (args.length === 1 && typeof args[0] === \"string\") {\n const line = args[0].replace(ANSI_PATTERN, \"\");\n if (SDK_WARNING_PREFIXES.some((prefix) => line.startsWith(prefix))) {\n write({ ev: \"log\", level: \"warn\", message: line });\n return;\n }\n }\n originalConsoleWarn(...args);\n};\n\nlet sdkPromise;\nfunction loadSdk() {\n // OPENCODE_CURSOR_SDK_PATH lets tests substitute a fake SDK module.\n sdkPromise ??= import(process.env.OPENCODE_CURSOR_SDK_PATH || \"@cursor/sdk\");\n return sdkPromise;\n}\n\n/** agentId -> SDKAgent */\nconst agents = new Map();\n/** send request id -> Run (for cancel) */\nconst runs = new Map();\n\nasync function handleRequest(req) {\n const { id, op } = req;\n switch (op) {\n case \"ping\": {\n write({ id, ok: true, pid: process.pid });\n return;\n }\n case \"create\":\n case \"resume\": {\n const { Agent } = await loadSdk();\n const agent =\n op === \"resume\"\n ? await Agent.resume(req.agentId, req.options)\n : await Agent.create(req.options);\n agents.set(agent.agentId, agent);\n write({ id, ok: true, agentId: agent.agentId });\n return;\n }\n case \"send\": {\n const agent = agents.get(req.agentId);\n if (!agent) throw new Error(`unknown agent \"${req.agentId}\"`);\n const sendOptions = {\n ...(req.mode ? { mode: req.mode } : {}),\n ...(req.force ? { local: { force: true } } : {}),\n ...(req.idempotencyKey ? { idempotencyKey: req.idempotencyKey } : {}),\n onDelta: ({ update }) => write({ id, ev: \"update\", update }),\n };\n const run = await agent.send(req.message, sendOptions);\n runs.set(id, run);\n // Acknowledge so the client can hand back a cancellable run handle.\n write({ id, ok: true });\n try {\n const result = await run.wait();\n write({ id, ev: \"result\", result });\n } catch (err) {\n write({ id, ev: \"error\", error: serializeError(err) });\n } finally {\n runs.delete(id);\n }\n return;\n }\n case \"cancel\": {\n const run = runs.get(req.sendId);\n if (run) await run.cancel();\n write({ id, ok: true });\n return;\n }\n case \"close\": {\n const agent = agents.get(req.agentId);\n agents.delete(req.agentId);\n try {\n agent?.close();\n } catch {\n // best effort\n }\n write({ id, ok: true });\n return;\n }\n default:\n throw new Error(`unknown op \"${op}\"`);\n }\n}\n\nconst rl = createInterface({ input: process.stdin });\nrl.on(\"line\", (line) => {\n if (!line.trim()) return;\n let req;\n try {\n req = JSON.parse(line);\n } catch (err) {\n write({ id: null, ok: false, error: serializeError(err) });\n return;\n }\n handleRequest(req).catch((err) => {\n write({ id: req.id, ok: false, error: serializeError(err) });\n });\n});\n\n// Parent gone (stdin closed) -> shut down; never outlive the plugin process.\nrl.on(\"close\", () => {\n process.exit(0);\n});\n"],"mappings":";AAoBA,SAAS,uBAAuB;AAIhC,SAAS,eAAe,KAAK;AAC3B,MAAI,eAAe,OAAO;AACxB,UAAM,MAAM,EAAE,MAAM,IAAI,MAAM,SAAS,IAAI,QAAQ;AACnD,eAAW,KAAK,CAAC,UAAU,QAAQ,eAAe,SAAS,GAAG;AAC5D,YAAM,IAAI,IAAI,CAAC;AACf,UACE,OAAO,MAAM,YACb,OAAO,MAAM,YACb,OAAO,MAAM;AAEb,YAAI,CAAC,IAAI;AAAA,IACb;AACA,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,SAAS,SAAS,OAAO,GAAG,EAAE;AAC/C;AAEA,SAAS,MAAM,SAAS;AACtB,UAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,OAAO,CAAC;AAAA,CAAI;AACrD;AAGA,IAAM,eAAe;AAYrB,IAAM,oBACJ;AAGF,IAAM,uBAAuB;AAAA,EAC3B;AACF;AAEA,SAAS,aAAa,KAAK;AACzB,QAAM,MAAM,CAAC;AACb,aAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,UAAM,CAAC,KAAK,KAAK,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACxD,QAAI,CAAC,OAAO,UAAU,OAAW;AACjC,UAAM,MAAM,OAAO,KAAK;AACxB,QAAI,OAAO,SAAS,GAAG,EAAG,KAAI,GAAG,IAAI;AAAA,EACvC;AACA,SAAO;AACT;AAEA,IAAM,qBAAqB,QAAQ,IAAI,KAAK,OAAO;AACnD,QAAQ,MAAM,IAAI,SAAS;AACzB,MAAI,KAAK,WAAW,KAAK,OAAO,KAAK,CAAC,MAAM,UAAU;AACpD,UAAM,QAAQ,kBAAkB,KAAK,KAAK,CAAC,EAAE,QAAQ,cAAc,EAAE,CAAC;AACtE,QAAI,OAAO;AACT,YAAM,CAAC,EAAE,SAAS,IAAI,IAAI;AAC1B,YAAM;AAAA,QACJ,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,SAAS,GAAG,OAAO;AAAA,QACnB,MAAM,aAAa,QAAQ,EAAE;AAAA,MAC/B,CAAC;AACD;AAAA,IACF;AAAA,EACF;AACA,qBAAmB,GAAG,IAAI;AAC5B;AAEA,IAAM,sBAAsB,QAAQ,KAAK,KAAK,OAAO;AACrD,QAAQ,OAAO,IAAI,SAAS;AAC1B,MAAI,KAAK,WAAW,KAAK,OAAO,KAAK,CAAC,MAAM,UAAU;AACpD,UAAM,OAAO,KAAK,CAAC,EAAE,QAAQ,cAAc,EAAE;AAC7C,QAAI,qBAAqB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC,GAAG;AAClE,YAAM,EAAE,IAAI,OAAO,OAAO,QAAQ,SAAS,KAAK,CAAC;AACjD;AAAA,IACF;AAAA,EACF;AACA,sBAAoB,GAAG,IAAI;AAC7B;AAEA,IAAI;AACJ,SAAS,UAAU;AAEjB,iBAAe,OAAO,QAAQ,IAAI,4BAA4B;AAC9D,SAAO;AACT;AAGA,IAAM,SAAS,oBAAI,IAAI;AAEvB,IAAM,OAAO,oBAAI,IAAI;AAErB,eAAe,cAAc,KAAK;AAChC,QAAM,EAAE,IAAI,GAAG,IAAI;AACnB,UAAQ,IAAI;AAAA,IACV,KAAK,QAAQ;AACX,YAAM,EAAE,IAAI,IAAI,MAAM,KAAK,QAAQ,IAAI,CAAC;AACxC;AAAA,IACF;AAAA,IACA,KAAK;AAAA,IACL,KAAK,UAAU;AACb,YAAM,EAAE,MAAM,IAAI,MAAM,QAAQ;AAChC,YAAM,QACJ,OAAO,WACH,MAAM,MAAM,OAAO,IAAI,SAAS,IAAI,OAAO,IAC3C,MAAM,MAAM,OAAO,IAAI,OAAO;AACpC,aAAO,IAAI,MAAM,SAAS,KAAK;AAC/B,YAAM,EAAE,IAAI,IAAI,MAAM,SAAS,MAAM,QAAQ,CAAC;AAC9C;AAAA,IACF;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,QAAQ,OAAO,IAAI,IAAI,OAAO;AACpC,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,kBAAkB,IAAI,OAAO,GAAG;AAC5D,YAAM,cAAc;AAAA,QAClB,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;AAAA,QACrC,GAAI,IAAI,QAAQ,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,IAAI,CAAC;AAAA,QAC9C,GAAI,IAAI,iBAAiB,EAAE,gBAAgB,IAAI,eAAe,IAAI,CAAC;AAAA,QACnE,SAAS,CAAC,EAAE,OAAO,MAAM,MAAM,EAAE,IAAI,IAAI,UAAU,OAAO,CAAC;AAAA,MAC7D;AACA,YAAM,MAAM,MAAM,MAAM,KAAK,IAAI,SAAS,WAAW;AACrD,WAAK,IAAI,IAAI,GAAG;AAEhB,YAAM,EAAE,IAAI,IAAI,KAAK,CAAC;AACtB,UAAI;AACF,cAAM,SAAS,MAAM,IAAI,KAAK;AAC9B,cAAM,EAAE,IAAI,IAAI,UAAU,OAAO,CAAC;AAAA,MACpC,SAAS,KAAK;AACZ,cAAM,EAAE,IAAI,IAAI,SAAS,OAAO,eAAe,GAAG,EAAE,CAAC;AAAA,MACvD,UAAE;AACA,aAAK,OAAO,EAAE;AAAA,MAChB;AACA;AAAA,IACF;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,KAAK,IAAI,IAAI,MAAM;AAC/B,UAAI,IAAK,OAAM,IAAI,OAAO;AAC1B,YAAM,EAAE,IAAI,IAAI,KAAK,CAAC;AACtB;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,QAAQ,OAAO,IAAI,IAAI,OAAO;AACpC,aAAO,OAAO,IAAI,OAAO;AACzB,UAAI;AACF,eAAO,MAAM;AAAA,MACf,QAAQ;AAAA,MAER;AACA,YAAM,EAAE,IAAI,IAAI,KAAK,CAAC;AACtB;AAAA,IACF;AAAA,IACA;AACE,YAAM,IAAI,MAAM,eAAe,EAAE,GAAG;AAAA,EACxC;AACF;AAEA,IAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,MAAM,CAAC;AACnD,GAAG,GAAG,QAAQ,CAAC,SAAS;AACtB,MAAI,CAAC,KAAK,KAAK,EAAG;AAClB,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,IAAI;AAAA,EACvB,SAAS,KAAK;AACZ,UAAM,EAAE,IAAI,MAAM,IAAI,OAAO,OAAO,eAAe,GAAG,EAAE,CAAC;AACzD;AAAA,EACF;AACA,gBAAc,GAAG,EAAE,MAAM,CAAC,QAAQ;AAChC,UAAM,EAAE,IAAI,IAAI,IAAI,IAAI,OAAO,OAAO,eAAe,GAAG,EAAE,CAAC;AAAA,EAC7D,CAAC;AACH,CAAC;AAGD,GAAG,GAAG,SAAS,MAAM;AACnB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* stdio MCP server that exposes opencode plugins' custom tools to the Cursor
|
|
6
|
+
* agent. Spawned by the plugin with `OPENCODE_PLUGIN_TOOLS_TOKEN` in env; it
|
|
7
|
+
* talks to the host plugin over a localhost HTTP control channel (also
|
|
8
|
+
* token-authenticated) that owns the real tool closures.
|
|
9
|
+
*
|
|
10
|
+
* Wire protocol (control channel):
|
|
11
|
+
* GET /tools → { tools: [{id, description, parameters}] }
|
|
12
|
+
* POST /call {id, args} → { ok, title?, output?, error? }
|
|
13
|
+
*
|
|
14
|
+
* Every `tools/call` from Cursor becomes one POST /call; the host executes
|
|
15
|
+
* the mirrored tool with a permission-gated ToolContext and returns the
|
|
16
|
+
* result. Plain JSON-lines stdio MCP on the other side (see run()).
|
|
17
|
+
*
|
|
18
|
+
* Kept as plain .mjs so tests can spawn it pre-build; tsup bundles it to
|
|
19
|
+
* dist/sidecar/plugin-tools-mcp.js for production.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const CONTROL_PORT = Number(process.env.OPENCODE_PLUGIN_TOOLS_PORT ?? 0);
|
|
23
|
+
const TOKEN = process.env.OPENCODE_PLUGIN_TOOLS_TOKEN ?? "";
|
|
24
|
+
const SERVER_NAME = "opencode-plugin-tools";
|
|
25
|
+
const SERVER_VERSION = "1.0.0";
|
|
26
|
+
// The single protocol version this server implements. Never echo the
|
|
27
|
+
// client-proposed version — MCP servers must answer with a version they
|
|
28
|
+
// actually support.
|
|
29
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
30
|
+
|
|
31
|
+
function logErr(message, extra) {
|
|
32
|
+
try {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
`[plugin-tools-mcp] ${message}${extra ? ` ${JSON.stringify(extra)}` : ""}\n`,
|
|
35
|
+
);
|
|
36
|
+
} catch {
|
|
37
|
+
// never throw from logging
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function controlRequest(path, body, timeoutMs) {
|
|
42
|
+
const res = await fetch(`http://127.0.0.1:${CONTROL_PORT}${path}`, {
|
|
43
|
+
method: body ? "POST" : "GET",
|
|
44
|
+
headers: {
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
authorization: `Bearer ${TOKEN}`,
|
|
47
|
+
},
|
|
48
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
49
|
+
// A stale/hung port must not hang Cursor's MCP discovery (tools/list) or
|
|
50
|
+
// block a tool call forever. Loopback list is instant; calls get a
|
|
51
|
+
// generous ceiling because plugin tools can legitimately run for minutes.
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
53
|
+
});
|
|
54
|
+
const text = await res.text();
|
|
55
|
+
let json;
|
|
56
|
+
try {
|
|
57
|
+
json = JSON.parse(text);
|
|
58
|
+
} catch {
|
|
59
|
+
json = undefined;
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const message = json?.error ?? `control channel ${res.status}`;
|
|
63
|
+
throw new Error(message);
|
|
64
|
+
}
|
|
65
|
+
return json;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function listTools() {
|
|
69
|
+
try {
|
|
70
|
+
const data = await controlRequest("/tools", undefined, 5_000);
|
|
71
|
+
return data?.tools ?? [];
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logErr("tools/list failed", { error: String(err) });
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function callTool(name, args) {
|
|
79
|
+
try {
|
|
80
|
+
const data = await controlRequest(
|
|
81
|
+
"/call",
|
|
82
|
+
{ id: name, args: args ?? {} },
|
|
83
|
+
300_000,
|
|
84
|
+
);
|
|
85
|
+
if (data?.ok === false) {
|
|
86
|
+
return {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: [{ type: "text", text: data.error ?? "tool call failed" }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const output = data?.output ?? "";
|
|
92
|
+
const title = data?.title ? `${data.title}\n\n` : "";
|
|
93
|
+
return { content: [{ type: "text", text: `${title}${output}` }] };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Handle one MCP request and return the response payload (or undefined for notifications). */
|
|
100
|
+
async function handle(msg) {
|
|
101
|
+
const { id, method, params } = msg;
|
|
102
|
+
const reply = (result) => ({ jsonrpc: "2.0", id, result });
|
|
103
|
+
const error = (code, message) => ({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
id,
|
|
106
|
+
error: { code, message },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
switch (method) {
|
|
110
|
+
case "initialize":
|
|
111
|
+
return reply({
|
|
112
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
113
|
+
capabilities: { tools: { listChanged: false } },
|
|
114
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
115
|
+
instructions:
|
|
116
|
+
"Tools provided by opencode plugins, bridged into the Cursor agent. " +
|
|
117
|
+
"Each call runs the plugin's real implementation inside opencode's runtime.",
|
|
118
|
+
});
|
|
119
|
+
case "notifications/initialized":
|
|
120
|
+
return undefined;
|
|
121
|
+
case "ping":
|
|
122
|
+
return reply({});
|
|
123
|
+
case "tools/list": {
|
|
124
|
+
const tools = await listTools();
|
|
125
|
+
return reply({
|
|
126
|
+
tools: tools.map((t) => ({
|
|
127
|
+
name: t.id,
|
|
128
|
+
description: t.description,
|
|
129
|
+
inputSchema: t.parameters ?? { type: "object", properties: {} },
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
case "tools/call": {
|
|
134
|
+
const result = await callTool(params?.name, params?.arguments);
|
|
135
|
+
return reply(result);
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
if (id === undefined) return undefined; // unknown notification
|
|
139
|
+
return error(-32601, `method not found: ${method}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function run() {
|
|
144
|
+
if (!CONTROL_PORT || !TOKEN) {
|
|
145
|
+
logErr("missing OPENCODE_PLUGIN_TOOLS_PORT/TOKEN env; exiting");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
149
|
+
rl.on("line", async (line) => {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed) return;
|
|
152
|
+
let msg;
|
|
153
|
+
try {
|
|
154
|
+
msg = JSON.parse(trimmed);
|
|
155
|
+
} catch {
|
|
156
|
+
process.stdout.write(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id: null,
|
|
160
|
+
error: { code: -32700, message: "parse error" },
|
|
161
|
+
}) + "\n",
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const response = await handle(msg);
|
|
167
|
+
if (response !== undefined) {
|
|
168
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (msg.id !== undefined) {
|
|
172
|
+
process.stdout.write(
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
jsonrpc: "2.0",
|
|
175
|
+
id: msg.id,
|
|
176
|
+
error: { code: -32603, message: String(err) },
|
|
177
|
+
}) + "\n",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
rl.on("close", () => process.exit(0));
|
|
183
|
+
// Keep the process alive until stdin closes (Cursor owns the lifecycle).
|
|
184
|
+
process.stdin.resume();
|
|
185
|
+
}
|
|
186
|
+
const scriptFile = fileURLToPath(import.meta.url);
|
|
187
|
+
if (process.argv[1] === scriptFile) run();
|
|
188
|
+
|
|
189
|
+
export { callTool, handle, listTools };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/sidecar/plugin-tools-mcp.mjs
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var CONTROL_PORT = Number(process.env.OPENCODE_PLUGIN_TOOLS_PORT ?? 0);
|
|
5
|
+
var TOKEN = process.env.OPENCODE_PLUGIN_TOOLS_TOKEN ?? "";
|
|
6
|
+
var SERVER_NAME = "opencode-plugin-tools";
|
|
7
|
+
var SERVER_VERSION = "1.0.0";
|
|
8
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
9
|
+
function logErr(message, extra) {
|
|
10
|
+
try {
|
|
11
|
+
process.stderr.write(
|
|
12
|
+
`[plugin-tools-mcp] ${message}${extra ? ` ${JSON.stringify(extra)}` : ""}
|
|
13
|
+
`
|
|
14
|
+
);
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function controlRequest(path, body, timeoutMs) {
|
|
19
|
+
const res = await fetch(`http://127.0.0.1:${CONTROL_PORT}${path}`, {
|
|
20
|
+
method: body ? "POST" : "GET",
|
|
21
|
+
headers: {
|
|
22
|
+
"content-type": "application/json",
|
|
23
|
+
authorization: `Bearer ${TOKEN}`
|
|
24
|
+
},
|
|
25
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
26
|
+
// A stale/hung port must not hang Cursor's MCP discovery (tools/list) or
|
|
27
|
+
// block a tool call forever. Loopback list is instant; calls get a
|
|
28
|
+
// generous ceiling because plugin tools can legitimately run for minutes.
|
|
29
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
30
|
+
});
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
let json;
|
|
33
|
+
try {
|
|
34
|
+
json = JSON.parse(text);
|
|
35
|
+
} catch {
|
|
36
|
+
json = void 0;
|
|
37
|
+
}
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const message = json?.error ?? `control channel ${res.status}`;
|
|
40
|
+
throw new Error(message);
|
|
41
|
+
}
|
|
42
|
+
return json;
|
|
43
|
+
}
|
|
44
|
+
async function listTools() {
|
|
45
|
+
try {
|
|
46
|
+
const data = await controlRequest("/tools", void 0, 5e3);
|
|
47
|
+
return data?.tools ?? [];
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logErr("tools/list failed", { error: String(err) });
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function callTool(name, args) {
|
|
54
|
+
try {
|
|
55
|
+
const data = await controlRequest(
|
|
56
|
+
"/call",
|
|
57
|
+
{ id: name, args: args ?? {} },
|
|
58
|
+
3e5
|
|
59
|
+
);
|
|
60
|
+
if (data?.ok === false) {
|
|
61
|
+
return {
|
|
62
|
+
isError: true,
|
|
63
|
+
content: [{ type: "text", text: data.error ?? "tool call failed" }]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const output = data?.output ?? "";
|
|
67
|
+
const title = data?.title ? `${data.title}
|
|
68
|
+
|
|
69
|
+
` : "";
|
|
70
|
+
return { content: [{ type: "text", text: `${title}${output}` }] };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function handle(msg) {
|
|
76
|
+
const { id, method, params } = msg;
|
|
77
|
+
const reply = (result) => ({ jsonrpc: "2.0", id, result });
|
|
78
|
+
const error = (code, message) => ({
|
|
79
|
+
jsonrpc: "2.0",
|
|
80
|
+
id,
|
|
81
|
+
error: { code, message }
|
|
82
|
+
});
|
|
83
|
+
switch (method) {
|
|
84
|
+
case "initialize":
|
|
85
|
+
return reply({
|
|
86
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
87
|
+
capabilities: { tools: { listChanged: false } },
|
|
88
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
89
|
+
instructions: "Tools provided by opencode plugins, bridged into the Cursor agent. Each call runs the plugin's real implementation inside opencode's runtime."
|
|
90
|
+
});
|
|
91
|
+
case "notifications/initialized":
|
|
92
|
+
return void 0;
|
|
93
|
+
case "ping":
|
|
94
|
+
return reply({});
|
|
95
|
+
case "tools/list": {
|
|
96
|
+
const tools = await listTools();
|
|
97
|
+
return reply({
|
|
98
|
+
tools: tools.map((t) => ({
|
|
99
|
+
name: t.id,
|
|
100
|
+
description: t.description,
|
|
101
|
+
inputSchema: t.parameters ?? { type: "object", properties: {} }
|
|
102
|
+
}))
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
case "tools/call": {
|
|
106
|
+
const result = await callTool(params?.name, params?.arguments);
|
|
107
|
+
return reply(result);
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
if (id === void 0) return void 0;
|
|
111
|
+
return error(-32601, `method not found: ${method}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function run() {
|
|
115
|
+
if (!CONTROL_PORT || !TOKEN) {
|
|
116
|
+
logErr("missing OPENCODE_PLUGIN_TOOLS_PORT/TOKEN env; exiting");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
120
|
+
rl.on("line", async (line) => {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (!trimmed) return;
|
|
123
|
+
let msg;
|
|
124
|
+
try {
|
|
125
|
+
msg = JSON.parse(trimmed);
|
|
126
|
+
} catch {
|
|
127
|
+
process.stdout.write(
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
jsonrpc: "2.0",
|
|
130
|
+
id: null,
|
|
131
|
+
error: { code: -32700, message: "parse error" }
|
|
132
|
+
}) + "\n"
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const response = await handle(msg);
|
|
138
|
+
if (response !== void 0) {
|
|
139
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (msg.id !== void 0) {
|
|
143
|
+
process.stdout.write(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
jsonrpc: "2.0",
|
|
146
|
+
id: msg.id,
|
|
147
|
+
error: { code: -32603, message: String(err) }
|
|
148
|
+
}) + "\n"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
rl.on("close", () => process.exit(0));
|
|
154
|
+
process.stdin.resume();
|
|
155
|
+
}
|
|
156
|
+
var scriptFile = fileURLToPath(import.meta.url);
|
|
157
|
+
if (process.argv[1] === scriptFile) run();
|
|
158
|
+
export {
|
|
159
|
+
callTool,
|
|
160
|
+
handle,
|
|
161
|
+
listTools
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=plugin-tools-mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sidecar/plugin-tools-mcp.mjs"],"sourcesContent":["/**\n * stdio MCP server that exposes opencode plugins' custom tools to the Cursor\n * agent. Spawned by the plugin with `OPENCODE_PLUGIN_TOOLS_TOKEN` in env; it\n * talks to the host plugin over a localhost HTTP control channel (also\n * token-authenticated) that owns the real tool closures.\n *\n * Wire protocol (control channel):\n * GET /tools → { tools: [{id, description, parameters}] }\n * POST /call {id, args} → { ok, title?, output?, error? }\n *\n * Every `tools/call` from Cursor becomes one POST /call; the host executes\n * the mirrored tool with a permission-gated ToolContext and returns the\n * result. Plain JSON-lines stdio MCP on the other side (see run()).\n *\n * Kept as plain .mjs so tests can spawn it pre-build; tsup bundles it to\n * dist/sidecar/plugin-tools-mcp.js for production.\n */\nimport { createInterface } from \"node:readline\";\n\nconst CONTROL_PORT = Number(process.env.OPENCODE_PLUGIN_TOOLS_PORT ?? 0);\nconst TOKEN = process.env.OPENCODE_PLUGIN_TOOLS_TOKEN ?? \"\";\nconst SERVER_NAME = \"opencode-plugin-tools\";\nconst SERVER_VERSION = \"1.0.0\";\n// The single protocol version this server implements. Never echo the\n// client-proposed version — MCP servers must answer with a version they\n// actually support.\nconst PROTOCOL_VERSION = \"2025-06-18\";\n\nfunction logErr(message, extra) {\n try {\n process.stderr.write(\n `[plugin-tools-mcp] ${message}${extra ? ` ${JSON.stringify(extra)}` : \"\"}\\n`,\n );\n } catch {\n // never throw from logging\n }\n}\n\nasync function controlRequest(path, body, timeoutMs) {\n const res = await fetch(`http://127.0.0.1:${CONTROL_PORT}${path}`, {\n method: body ? \"POST\" : \"GET\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${TOKEN}`,\n },\n body: body ? JSON.stringify(body) : undefined,\n // A stale/hung port must not hang Cursor's MCP discovery (tools/list) or\n // block a tool call forever. Loopback list is instant; calls get a\n // generous ceiling because plugin tools can legitimately run for minutes.\n signal: AbortSignal.timeout(timeoutMs),\n });\n const text = await res.text();\n let json;\n try {\n json = JSON.parse(text);\n } catch {\n json = undefined;\n }\n if (!res.ok) {\n const message = json?.error ?? `control channel ${res.status}`;\n throw new Error(message);\n }\n return json;\n}\n\nasync function listTools() {\n try {\n const data = await controlRequest(\"/tools\", undefined, 5_000);\n return data?.tools ?? [];\n } catch (err) {\n logErr(\"tools/list failed\", { error: String(err) });\n return [];\n }\n}\n\nasync function callTool(name, args) {\n try {\n const data = await controlRequest(\n \"/call\",\n { id: name, args: args ?? {} },\n 300_000,\n );\n if (data?.ok === false) {\n return {\n isError: true,\n content: [{ type: \"text\", text: data.error ?? \"tool call failed\" }],\n };\n }\n const output = data?.output ?? \"\";\n const title = data?.title ? `${data.title}\\n\\n` : \"\";\n return { content: [{ type: \"text\", text: `${title}${output}` }] };\n } catch (err) {\n return { isError: true, content: [{ type: \"text\", text: String(err) }] };\n }\n}\n\n/** Handle one MCP request and return the response payload (or undefined for notifications). */\nasync function handle(msg) {\n const { id, method, params } = msg;\n const reply = (result) => ({ jsonrpc: \"2.0\", id, result });\n const error = (code, message) => ({\n jsonrpc: \"2.0\",\n id,\n error: { code, message },\n });\n\n switch (method) {\n case \"initialize\":\n return reply({\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: { listChanged: false } },\n serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },\n instructions:\n \"Tools provided by opencode plugins, bridged into the Cursor agent. \" +\n \"Each call runs the plugin's real implementation inside opencode's runtime.\",\n });\n case \"notifications/initialized\":\n return undefined;\n case \"ping\":\n return reply({});\n case \"tools/list\": {\n const tools = await listTools();\n return reply({\n tools: tools.map((t) => ({\n name: t.id,\n description: t.description,\n inputSchema: t.parameters ?? { type: \"object\", properties: {} },\n })),\n });\n }\n case \"tools/call\": {\n const result = await callTool(params?.name, params?.arguments);\n return reply(result);\n }\n default:\n if (id === undefined) return undefined; // unknown notification\n return error(-32601, `method not found: ${method}`);\n }\n}\n\nfunction run() {\n if (!CONTROL_PORT || !TOKEN) {\n logErr(\"missing OPENCODE_PLUGIN_TOOLS_PORT/TOKEN env; exiting\");\n process.exit(1);\n }\n const rl = createInterface({ input: process.stdin, terminal: false });\n rl.on(\"line\", async (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n let msg;\n try {\n msg = JSON.parse(trimmed);\n } catch {\n process.stdout.write(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: null,\n error: { code: -32700, message: \"parse error\" },\n }) + \"\\n\",\n );\n return;\n }\n try {\n const response = await handle(msg);\n if (response !== undefined) {\n process.stdout.write(JSON.stringify(response) + \"\\n\");\n }\n } catch (err) {\n if (msg.id !== undefined) {\n process.stdout.write(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: msg.id,\n error: { code: -32603, message: String(err) },\n }) + \"\\n\",\n );\n }\n }\n });\n rl.on(\"close\", () => process.exit(0));\n // Keep the process alive until stdin closes (Cursor owns the lifecycle).\n process.stdin.resume();\n}\n\n// Start the stdio loop only when spawned as the entry script (Cursor owns\n// the lifecycle). Tests import the handlers above without triggering it.\nimport { fileURLToPath } from \"node:url\";\nconst scriptFile = fileURLToPath(import.meta.url);\nif (process.argv[1] === scriptFile) run();\n\nexport { handle, listTools, callTool };\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stablekernel/opencode-cursor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-next.1",
|
|
4
4
|
"description": "opencode provider plugin backed by the official Cursor SDK (@cursor/sdk) — adds a Cursor provider and lists its models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@connectrpc/connect-node": "^2.1.2",
|
|
61
61
|
"@cursor/sdk": "^1.0.24",
|
|
62
|
-
"@opencode-ai/plugin": "^1.18.
|
|
62
|
+
"@opencode-ai/plugin": "^1.18.21",
|
|
63
63
|
"semver": "^7.8.4"
|
|
64
64
|
},
|
|
65
65
|
"overrides": {
|
|
66
|
-
"undici": "^6.
|
|
66
|
+
"undici": "^6.28.0",
|
|
67
67
|
"tar": "^7.5.11",
|
|
68
68
|
"node-gyp": "^12.4.0",
|
|
69
69
|
"esbuild": "^0.28.1"
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"@ai-sdk/provider": "^3.0.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@ai-sdk/provider": "^3.0.
|
|
76
|
-
"@opencode-ai/sdk": "^1.18.
|
|
77
|
-
"@types/node": "^26.
|
|
78
|
-
"@types/semver": "^7.
|
|
75
|
+
"@ai-sdk/provider": "^3.0.15",
|
|
76
|
+
"@opencode-ai/sdk": "^1.18.21",
|
|
77
|
+
"@types/node": "^26.2.0",
|
|
78
|
+
"@types/semver": "^7.8.0",
|
|
79
79
|
"tsup": "^8.5.1",
|
|
80
80
|
"typescript": "^6.0.3",
|
|
81
81
|
"vitest": "^4.1.8"
|