@rama_nigg/open-cursor 2.3.15 → 2.3.17
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/README.md +87 -23
- package/dist/cli/discover.js +30 -0
- package/dist/cli/mcptool.js +15187 -0
- package/dist/cli/opencode-cursor.js +30 -0
- package/dist/index.js +15687 -584
- package/dist/plugin-entry.js +15657 -554
- package/package.json +7 -5
- package/src/cli/mcptool.ts +133 -0
- package/src/mcp/client-manager.ts +166 -0
- package/src/mcp/config.ts +98 -0
- package/src/mcp/tool-bridge.ts +131 -0
- package/src/plugin.ts +124 -7
- package/src/tools/defaults.ts +50 -18
package/src/plugin.ts
CHANGED
|
@@ -25,6 +25,9 @@ import { ToolRouter } from "./tools/router.js";
|
|
|
25
25
|
import { SkillLoader } from "./tools/skills/loader.js";
|
|
26
26
|
import { SkillResolver } from "./tools/skills/resolver.js";
|
|
27
27
|
import { autoRefreshModels } from "./models/sync.js";
|
|
28
|
+
import { readMcpConfigs } from "./mcp/config.js";
|
|
29
|
+
import { McpClientManager } from "./mcp/client-manager.js";
|
|
30
|
+
import { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
|
|
28
31
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
29
32
|
import { ToolRegistry as CoreRegistry } from "./tools/core/registry.js";
|
|
30
33
|
import { LocalExecutor } from "./tools/executors/local.js";
|
|
@@ -77,6 +80,61 @@ function debugLogToFile(message: string, data: any): void {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
interface McpToolSummary {
|
|
84
|
+
serverName: string;
|
|
85
|
+
toolName: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
params?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildAvailableToolsSystemMessage(
|
|
91
|
+
lastToolNames: string[],
|
|
92
|
+
lastToolMap: Array<{ id: string; name: string }>,
|
|
93
|
+
mcpToolDefs: any[],
|
|
94
|
+
mcpToolSummaries?: McpToolSummary[],
|
|
95
|
+
): string | null {
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
|
|
98
|
+
if (lastToolNames.length > 0 || lastToolMap.length > 0) {
|
|
99
|
+
const names = lastToolNames.join(", ");
|
|
100
|
+
const mapping = lastToolMap.map((m) => `${m.id} -> ${m.name}`).join("; ");
|
|
101
|
+
parts.push(`Available OpenCode tools (use via tool calls): ${names}. Original skill ids mapped as: ${mapping}. Aliases include oc_skill_* and oc_superskill_* when applicable.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (mcpToolSummaries && mcpToolSummaries.length > 0) {
|
|
105
|
+
const servers = new Map<string, McpToolSummary[]>();
|
|
106
|
+
for (const s of mcpToolSummaries) {
|
|
107
|
+
const list = servers.get(s.serverName) ?? [];
|
|
108
|
+
list.push(s);
|
|
109
|
+
servers.set(s.serverName, list);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const lines: string[] = [
|
|
113
|
+
"MCP TOOLS — Use via Shell with the `mcptool` CLI.",
|
|
114
|
+
"Syntax: mcptool call <server> <tool> [json-args]",
|
|
115
|
+
"",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const [server, tools] of servers) {
|
|
119
|
+
lines.push(`Server: ${server}`);
|
|
120
|
+
for (const t of tools) {
|
|
121
|
+
const paramHint = t.params?.length ? ` (params: ${t.params.join(", ")})` : "";
|
|
122
|
+
lines.push(` - ${t.toolName}${paramHint}${t.description ? " — " + t.description : ""}`);
|
|
123
|
+
}
|
|
124
|
+
if (tools.length > 0) {
|
|
125
|
+
const ex = tools[0];
|
|
126
|
+
const exArgs = ex.params?.length ? ` '{"${ex.params[0]}":"..."}'` : "";
|
|
127
|
+
lines.push(` Example: mcptool call ${server} ${ex.toolName}${exArgs}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
parts.push(lines.join("\n"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
80
138
|
export async function ensurePluginDirectory(): Promise<void> {
|
|
81
139
|
const configHome = process.env.XDG_CONFIG_HOME
|
|
82
140
|
? resolve(process.env.XDG_CONFIG_HOME)
|
|
@@ -1739,6 +1797,49 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1739
1797
|
// Auto-refresh model list from cursor-agent (non-blocking, fire-and-forget)
|
|
1740
1798
|
autoRefreshModels().catch(() => {});
|
|
1741
1799
|
|
|
1800
|
+
// MCP tool bridge: connect to MCP servers and register their tools.
|
|
1801
|
+
// We await init so tools are available before the plugin returns its tool hook.
|
|
1802
|
+
const mcpManager = new McpClientManager();
|
|
1803
|
+
let mcpToolEntries: Record<string, any> = {};
|
|
1804
|
+
let mcpToolDefs: any[] = [];
|
|
1805
|
+
let mcpToolSummaries: McpToolSummary[] = [];
|
|
1806
|
+
const mcpEnabled = process.env.CURSOR_ACP_MCP_BRIDGE !== "false"; // default ON
|
|
1807
|
+
|
|
1808
|
+
if (mcpEnabled) {
|
|
1809
|
+
try {
|
|
1810
|
+
const configs = readMcpConfigs();
|
|
1811
|
+
if (configs.length === 0) {
|
|
1812
|
+
log.debug("No MCP servers configured, skipping MCP bridge");
|
|
1813
|
+
} else {
|
|
1814
|
+
log.debug("MCP bridge: connecting to servers", { count: configs.length });
|
|
1815
|
+
|
|
1816
|
+
await Promise.allSettled(configs.map((c) => mcpManager.connectServer(c)));
|
|
1817
|
+
|
|
1818
|
+
const tools = mcpManager.listTools();
|
|
1819
|
+
if (tools.length === 0) {
|
|
1820
|
+
log.debug("MCP bridge: no tools discovered");
|
|
1821
|
+
} else {
|
|
1822
|
+
mcpToolEntries = buildMcpToolHookEntries(tools, mcpManager);
|
|
1823
|
+
mcpToolDefs = buildMcpToolDefinitions(tools);
|
|
1824
|
+
mcpToolSummaries = tools.map((t) => ({
|
|
1825
|
+
serverName: t.serverName,
|
|
1826
|
+
toolName: t.name,
|
|
1827
|
+
description: t.description,
|
|
1828
|
+
params: t.inputSchema
|
|
1829
|
+
? Object.keys((t.inputSchema as any).properties ?? {})
|
|
1830
|
+
: undefined,
|
|
1831
|
+
}));
|
|
1832
|
+
log.info("MCP bridge: registered tools", {
|
|
1833
|
+
servers: mcpManager.connectedServers.length,
|
|
1834
|
+
tools: Object.keys(mcpToolEntries).length,
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
log.debug("MCP bridge init failed", { error: String(err) });
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1742
1843
|
// Initialize toast service for MCP pass-through notifications
|
|
1743
1844
|
toastService.setClient(client);
|
|
1744
1845
|
|
|
@@ -1862,7 +1963,7 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1862
1963
|
const toolHookEntries = buildToolHookEntries(localRegistry, workspaceDirectory);
|
|
1863
1964
|
|
|
1864
1965
|
return {
|
|
1865
|
-
tool: toolHookEntries,
|
|
1966
|
+
tool: { ...toolHookEntries, ...mcpToolEntries },
|
|
1866
1967
|
auth: {
|
|
1867
1968
|
provider: CURSOR_PROVIDER_ID,
|
|
1868
1969
|
async loader(_getAuth: () => Promise<Auth>) {
|
|
@@ -1935,16 +2036,32 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1935
2036
|
log.debug("Failed to refresh tools", { error: String(err) });
|
|
1936
2037
|
}
|
|
1937
2038
|
}
|
|
2039
|
+
|
|
2040
|
+
// Append MCP bridge tool definitions so the model can call them
|
|
2041
|
+
if (mcpToolDefs.length > 0) {
|
|
2042
|
+
const beforeTools = Array.isArray(output.options.tools) ? output.options.tools : [];
|
|
2043
|
+
if (Array.isArray(output.options.tools)) {
|
|
2044
|
+
output.options.tools = [...output.options.tools, ...mcpToolDefs];
|
|
2045
|
+
} else {
|
|
2046
|
+
output.options.tools = mcpToolDefs;
|
|
2047
|
+
}
|
|
2048
|
+
const afterTools = Array.isArray(output.options.tools) ? output.options.tools : [];
|
|
2049
|
+
log.debug("Injected MCP tool definitions into chat.params", {
|
|
2050
|
+
injectedCount: mcpToolDefs.length,
|
|
2051
|
+
beforeCount: beforeTools.length,
|
|
2052
|
+
afterCount: afterTools.length,
|
|
2053
|
+
mcpNames: mcpToolDefs.slice(0, 10).map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
|
|
2054
|
+
tailNames: afterTools.slice(-10).map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
1938
2057
|
},
|
|
1939
2058
|
|
|
1940
2059
|
async "experimental.chat.system.transform"(input: any, output: { system: string[] }) {
|
|
1941
|
-
if (!toolsEnabled
|
|
1942
|
-
const
|
|
1943
|
-
|
|
2060
|
+
if (!toolsEnabled) return;
|
|
2061
|
+
const systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
|
|
2062
|
+
if (!systemMessage) return;
|
|
1944
2063
|
output.system = output.system || [];
|
|
1945
|
-
output.system.push(
|
|
1946
|
-
`Available OpenCode tools (use via tool calls): ${names}. Original skill ids mapped as: ${mapping}. Aliases include oc_skill_* and oc_superskill_* when applicable.`
|
|
1947
|
-
);
|
|
2064
|
+
output.system.push(systemMessage);
|
|
1948
2065
|
},
|
|
1949
2066
|
};
|
|
1950
2067
|
};
|
package/src/tools/defaults.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function registerDefaultTools(registry: ToolRegistry): void {
|
|
|
18
18
|
},
|
|
19
19
|
timeout: {
|
|
20
20
|
type: "number",
|
|
21
|
-
description: "Timeout in
|
|
21
|
+
description: "Timeout in seconds (default: 30)"
|
|
22
22
|
},
|
|
23
23
|
cwd: {
|
|
24
24
|
type: "string",
|
|
@@ -29,25 +29,49 @@ export function registerDefaultTools(registry: ToolRegistry): void {
|
|
|
29
29
|
},
|
|
30
30
|
source: "local" as const
|
|
31
31
|
}, async (args) => {
|
|
32
|
-
const {
|
|
33
|
-
const { promisify } = await import("util");
|
|
34
|
-
const execAsync = promisify(exec);
|
|
32
|
+
const { spawn } = await import("child_process");
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error("bash: missing required argument 'command'");
|
|
40
|
-
}
|
|
41
|
-
const timeout = resolveTimeout(args.timeout);
|
|
42
|
-
const cwd = resolveWorkingDirectory(args);
|
|
43
|
-
const { stdout, stderr } = await execAsync(command, {
|
|
44
|
-
timeout: timeout ?? 30000,
|
|
45
|
-
cwd: cwd
|
|
46
|
-
});
|
|
47
|
-
return stdout || stderr || "Command executed successfully";
|
|
48
|
-
} catch (error: any) {
|
|
49
|
-
throw error;
|
|
34
|
+
const command = resolveBashCommand(args);
|
|
35
|
+
if (!command) {
|
|
36
|
+
throw new Error("bash: missing required argument 'command'");
|
|
50
37
|
}
|
|
38
|
+
const timeoutMs = resolveTimeoutMs(args.timeout);
|
|
39
|
+
const cwd = resolveWorkingDirectory(args);
|
|
40
|
+
|
|
41
|
+
return new Promise<string>((resolve, reject) => {
|
|
42
|
+
const proc = spawn(command, {
|
|
43
|
+
shell: process.env.SHELL || "/bin/bash",
|
|
44
|
+
cwd,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const stdoutChunks: Buffer[] = [];
|
|
48
|
+
const stderrChunks: Buffer[] = [];
|
|
49
|
+
let timedOut = false;
|
|
50
|
+
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
timedOut = true;
|
|
53
|
+
proc.kill("SIGTERM");
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
|
|
56
|
+
proc.stdout.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
57
|
+
proc.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
58
|
+
|
|
59
|
+
proc.on("close", (code) => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
62
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
63
|
+
const output = stdout || stderr || "Command executed successfully";
|
|
64
|
+
if (timedOut) {
|
|
65
|
+
resolve(`Command timed out after ${timeoutMs / 1000}s\n${output}`);
|
|
66
|
+
} else if (code !== 0) {
|
|
67
|
+
resolve(`${output}\n[Exit code: ${code}]`);
|
|
68
|
+
} else {
|
|
69
|
+
resolve(output);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
proc.on("error", reject);
|
|
74
|
+
});
|
|
51
75
|
});
|
|
52
76
|
|
|
53
77
|
// 2. Read tool - Read file contents
|
|
@@ -598,6 +622,14 @@ function resolveTimeout(value: unknown): number | undefined {
|
|
|
598
622
|
return undefined;
|
|
599
623
|
}
|
|
600
624
|
|
|
625
|
+
// Convert model-supplied timeout (seconds) to milliseconds. Falls back to 30s.
|
|
626
|
+
function resolveTimeoutMs(value: unknown): number {
|
|
627
|
+
const raw = resolveTimeout(value);
|
|
628
|
+
if (raw === undefined) return 30_000;
|
|
629
|
+
// Values ≤ 600 are treated as seconds (no real use case for a <600ms shell timeout).
|
|
630
|
+
return raw <= 600 ? raw * 1000 : raw;
|
|
631
|
+
}
|
|
632
|
+
|
|
601
633
|
function resolveBoolean(value: unknown, defaultValue: boolean): boolean {
|
|
602
634
|
if (typeof value === "boolean") {
|
|
603
635
|
return value;
|