@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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@rama_nigg/open-cursor",
3
- "version": "2.3.15",
3
+ "version": "2.3.17",
4
4
  "description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
5
5
  "type": "module",
6
6
  "main": "dist/plugin-entry.js",
7
7
  "module": "src/plugin-entry.ts",
8
8
  "scripts": {
9
- "build": "bun build ./src/index.ts ./src/plugin-entry.ts ./src/cli/discover.ts ./src/cli/opencode-cursor.ts --outdir ./dist --target node",
10
- "dev": "bun build ./src/index.ts ./src/plugin-entry.ts ./src/cli/discover.ts ./src/cli/opencode-cursor.ts --outdir ./dist --target node --watch",
9
+ "build": "bun build ./src/index.ts ./src/plugin-entry.ts ./src/cli/discover.ts ./src/cli/opencode-cursor.ts ./src/cli/mcptool.ts --outdir ./dist --target node",
10
+ "dev": "bun build ./src/index.ts ./src/plugin-entry.ts ./src/cli/discover.ts ./src/cli/opencode-cursor.ts ./src/cli/mcptool.ts --outdir ./dist --target node --watch",
11
11
  "test": "bun test",
12
12
  "test:unit": "bun test tests/unit",
13
13
  "test:integration": "bun test tests/integration",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "bin": {
20
20
  "open-cursor": "dist/cli/opencode-cursor.js",
21
- "cursor-discover": "dist/cli/discover.js"
21
+ "cursor-discover": "dist/cli/discover.js",
22
+ "mcptool": "dist/cli/mcptool.js"
22
23
  },
23
24
  "exports": {
24
25
  ".": {
@@ -37,9 +38,10 @@
37
38
  "src"
38
39
  ],
39
40
  "dependencies": {
40
- "ai": "^6.0.55",
41
+ "@modelcontextprotocol/sdk": "^1.12.0",
41
42
  "@opencode-ai/plugin": "1.1.53",
42
43
  "@opencode-ai/sdk": "1.1.53",
44
+ "ai": "^6.0.55",
43
45
  "strip-ansi": "^7.1.0"
44
46
  },
45
47
  "devDependencies": {
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mcptool — CLI for calling MCP server tools from the shell.
5
+ *
6
+ * Usage:
7
+ * mcptool servers List configured MCP servers
8
+ * mcptool tools [server] List tools (optionally filter by server)
9
+ * mcptool call <server> <tool> [json-args] Call a tool
10
+ *
11
+ * Reads MCP server configuration from opencode.json (same config the plugin uses).
12
+ */
13
+
14
+ import { readMcpConfigs } from "../mcp/config.js";
15
+ import { McpClientManager } from "../mcp/client-manager.js";
16
+
17
+ const USAGE = `mcptool — call MCP server tools from the shell
18
+
19
+ Usage:
20
+ mcptool servers List configured servers
21
+ mcptool tools [server] List available tools
22
+ mcptool call <server> <tool> [json-args] Call a tool
23
+
24
+ Examples:
25
+ mcptool servers
26
+ mcptool tools
27
+ mcptool tools hybrid-memory
28
+ mcptool call hybrid-memory memory_stats
29
+ mcptool call hybrid-memory memory_search '{"query":"auth"}'
30
+ mcptool call test-filesystem list_directory '{"path":"/tmp"}'`;
31
+
32
+ async function main(): Promise<void> {
33
+ const args = process.argv.slice(2);
34
+
35
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
36
+ console.log(USAGE);
37
+ process.exit(0);
38
+ }
39
+
40
+ const command = args[0];
41
+ const configs = readMcpConfigs();
42
+
43
+ if (configs.length === 0) {
44
+ console.error("No MCP servers configured in opencode.json");
45
+ process.exit(1);
46
+ }
47
+
48
+ const manager = new McpClientManager();
49
+
50
+ if (command === "servers") {
51
+ for (const c of configs) {
52
+ const detail =
53
+ c.type === "local" ? c.command.join(" ") : (c as any).url ?? "";
54
+ console.log(`${c.name} (${c.type}) ${detail}`);
55
+ }
56
+ process.exit(0);
57
+ }
58
+
59
+ if (command === "tools") {
60
+ const filter = args[1];
61
+ const toConnect = filter
62
+ ? configs.filter((c) => c.name === filter)
63
+ : configs;
64
+
65
+ if (filter && toConnect.length === 0) {
66
+ console.error(`Unknown server: ${filter}`);
67
+ console.error(`Available: ${configs.map((c) => c.name).join(", ")}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ await Promise.allSettled(toConnect.map((c) => manager.connectServer(c)));
72
+ const tools = manager.listTools();
73
+
74
+ if (tools.length === 0) {
75
+ console.log("No tools discovered.");
76
+ } else {
77
+ for (const t of tools) {
78
+ const params = t.inputSchema
79
+ ? Object.keys((t.inputSchema as any).properties ?? {}).join(", ")
80
+ : "";
81
+ console.log(`${t.serverName}/${t.name} ${t.description ?? ""}`);
82
+ if (params) console.log(` params: ${params}`);
83
+ }
84
+ }
85
+
86
+ await manager.disconnectAll();
87
+ process.exit(0);
88
+ }
89
+
90
+ if (command === "call") {
91
+ const serverName = args[1];
92
+ const toolName = args[2];
93
+ const rawArgs = args[3];
94
+
95
+ if (!serverName || !toolName) {
96
+ console.error("Usage: mcptool call <server> <tool> [json-args]");
97
+ process.exit(1);
98
+ }
99
+
100
+ const config = configs.find((c) => c.name === serverName);
101
+ if (!config) {
102
+ console.error(`Unknown server: ${serverName}`);
103
+ console.error(`Available: ${configs.map((c) => c.name).join(", ")}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ let toolArgs: Record<string, unknown> = {};
108
+ if (rawArgs) {
109
+ try {
110
+ toolArgs = JSON.parse(rawArgs);
111
+ } catch {
112
+ console.error(`Invalid JSON args: ${rawArgs}`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ await manager.connectServer(config);
118
+ const result = await manager.callTool(serverName, toolName, toolArgs);
119
+ console.log(result);
120
+
121
+ await manager.disconnectAll();
122
+ process.exit(0);
123
+ }
124
+
125
+ console.error(`Unknown command: ${command}`);
126
+ console.log(USAGE);
127
+ process.exit(1);
128
+ }
129
+
130
+ main().catch((err) => {
131
+ console.error(`mcptool error: ${err.message || err}`);
132
+ process.exit(1);
133
+ });
@@ -0,0 +1,166 @@
1
+ import { createLogger } from "../utils/logger.js";
2
+ import type { McpServerConfig } from "./config.js";
3
+
4
+ const log = createLogger("mcp:client-manager");
5
+
6
+ export interface McpToolInfo {
7
+ name: string;
8
+ description?: string;
9
+ inputSchema?: Record<string, unknown>;
10
+ }
11
+
12
+ interface DiscoveredTool extends McpToolInfo {
13
+ serverName: string;
14
+ }
15
+
16
+ interface ServerConnection {
17
+ client: any;
18
+ tools: McpToolInfo[];
19
+ }
20
+
21
+ interface McpClientManagerDeps {
22
+ createClient: () => any;
23
+ createTransport: (config: McpServerConfig) => any;
24
+ }
25
+
26
+ let defaultDeps: McpClientManagerDeps | null = null;
27
+
28
+ async function loadDefaultDeps(): Promise<McpClientManagerDeps> {
29
+ if (defaultDeps) return defaultDeps;
30
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
31
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
32
+
33
+ defaultDeps = {
34
+ createClient: () =>
35
+ new Client({ name: "open-cursor", version: "1.0.0" }, { capabilities: {} }),
36
+ createTransport: (config: McpServerConfig) => {
37
+ if (config.type === "local") {
38
+ return new StdioClientTransport({
39
+ command: config.command[0],
40
+ args: config.command.slice(1),
41
+ env: { ...process.env, ...(config.environment ?? {}) },
42
+ stderr: "pipe",
43
+ });
44
+ }
45
+ // Remote servers: StreamableHTTPClientTransport can be added later.
46
+ throw new Error(`Remote MCP transport not yet implemented for ${config.name}`);
47
+ },
48
+ };
49
+ return defaultDeps;
50
+ }
51
+
52
+ export class McpClientManager {
53
+ private connections = new Map<string, ServerConnection>();
54
+ private deps: McpClientManagerDeps | null;
55
+
56
+ constructor(deps?: McpClientManagerDeps) {
57
+ this.deps = deps ?? null;
58
+ }
59
+
60
+ async connectServer(config: McpServerConfig): Promise<void> {
61
+ if (this.connections.has(config.name)) {
62
+ log.debug("Server already connected, skipping", { server: config.name });
63
+ return;
64
+ }
65
+
66
+ // Lazy-load MCP SDK if no deps were injected
67
+ if (!this.deps) {
68
+ try {
69
+ this.deps = await loadDefaultDeps();
70
+ } catch (err) {
71
+ log.warn("Failed to load MCP SDK", { error: String(err) });
72
+ return;
73
+ }
74
+ }
75
+
76
+ const deps = this.deps;
77
+ let client: any;
78
+ try {
79
+ client = deps.createClient();
80
+ const transport = deps.createTransport(config);
81
+ await client.connect(transport);
82
+ } catch (err) {
83
+ log.warn("MCP server connection failed", {
84
+ server: config.name,
85
+ error: String(err),
86
+ });
87
+ return;
88
+ }
89
+
90
+ let tools: McpToolInfo[] = [];
91
+ try {
92
+ const result = await client.listTools();
93
+ tools = result?.tools ?? [];
94
+ log.info("MCP server connected", {
95
+ server: config.name,
96
+ tools: tools.length,
97
+ });
98
+ } catch (err) {
99
+ log.warn("MCP tool discovery failed", {
100
+ server: config.name,
101
+ error: String(err),
102
+ });
103
+ }
104
+
105
+ this.connections.set(config.name, { client, tools });
106
+ }
107
+
108
+ listTools(): DiscoveredTool[] {
109
+ const all: DiscoveredTool[] = [];
110
+ for (const [serverName, conn] of this.connections) {
111
+ for (const tool of conn.tools) {
112
+ all.push({ ...tool, serverName });
113
+ }
114
+ }
115
+ return all;
116
+ }
117
+
118
+ async callTool(
119
+ serverName: string,
120
+ toolName: string,
121
+ args: Record<string, unknown>,
122
+ ): Promise<string> {
123
+ const conn = this.connections.get(serverName);
124
+ if (!conn) {
125
+ return `Error: MCP server "${serverName}" not connected`;
126
+ }
127
+
128
+ try {
129
+ const result = await conn.client.callTool({
130
+ name: toolName,
131
+ arguments: args,
132
+ });
133
+
134
+ // MCP callTool returns { content: Array<{ type, text }> }
135
+ if (Array.isArray(result?.content)) {
136
+ return result.content
137
+ .map((c: any) => (c.type === "text" ? c.text : JSON.stringify(c)))
138
+ .join("\n");
139
+ }
140
+ return typeof result === "string" ? result : JSON.stringify(result);
141
+ } catch (err: any) {
142
+ log.warn("MCP tool call failed", {
143
+ server: serverName,
144
+ tool: toolName,
145
+ error: String(err?.message || err),
146
+ });
147
+ return `Error: MCP tool "${toolName}" failed: ${err?.message || err}`;
148
+ }
149
+ }
150
+
151
+ async disconnectAll(): Promise<void> {
152
+ for (const [name, conn] of this.connections) {
153
+ try {
154
+ await conn.client.close();
155
+ log.debug("MCP server disconnected", { server: name });
156
+ } catch (err) {
157
+ log.debug("MCP server disconnect failed", { server: name, error: String(err) });
158
+ }
159
+ }
160
+ this.connections.clear();
161
+ }
162
+
163
+ get connectedServers(): string[] {
164
+ return Array.from(this.connections.keys());
165
+ }
166
+ }
@@ -0,0 +1,98 @@
1
+ import {
2
+ existsSync as nodeExistsSync,
3
+ readFileSync as nodeReadFileSync,
4
+ } from "node:fs";
5
+ import { resolveOpenCodeConfigPath } from "../plugin-toggle.js";
6
+ import { createLogger } from "../utils/logger.js";
7
+
8
+ const log = createLogger("mcp:config");
9
+
10
+ export type McpLocalServerConfig = {
11
+ name: string;
12
+ type: "local";
13
+ command: string[];
14
+ environment?: Record<string, string>;
15
+ timeout?: number;
16
+ };
17
+
18
+ export type McpRemoteServerConfig = {
19
+ name: string;
20
+ type: "remote";
21
+ url: string;
22
+ headers?: Record<string, string>;
23
+ timeout?: number;
24
+ };
25
+
26
+ export type McpServerConfig = McpLocalServerConfig | McpRemoteServerConfig;
27
+
28
+ interface ReadMcpConfigsDeps {
29
+ configJson?: string;
30
+ existsSync?: (path: string) => boolean;
31
+ readFileSync?: (path: string, enc: BufferEncoding) => string;
32
+ env?: NodeJS.ProcessEnv;
33
+ }
34
+
35
+ export function readMcpConfigs(deps: ReadMcpConfigsDeps = {}): McpServerConfig[] {
36
+ let raw: string;
37
+
38
+ if (deps.configJson != null) {
39
+ raw = deps.configJson;
40
+ } else {
41
+ const exists = deps.existsSync ?? nodeExistsSync;
42
+ const readFile = deps.readFileSync ?? nodeReadFileSync;
43
+ const configPath = resolveOpenCodeConfigPath(deps.env ?? process.env);
44
+ if (!exists(configPath)) return [];
45
+ try {
46
+ raw = readFile(configPath, "utf8");
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ let parsed: Record<string, unknown>;
53
+ try {
54
+ parsed = JSON.parse(raw);
55
+ } catch {
56
+ return [];
57
+ }
58
+
59
+ const mcpSection = parsed.mcp;
60
+ if (!mcpSection || typeof mcpSection !== "object" || Array.isArray(mcpSection)) {
61
+ return [];
62
+ }
63
+
64
+ const configs: McpServerConfig[] = [];
65
+
66
+ for (const [name, entry] of Object.entries(mcpSection as Record<string, unknown>)) {
67
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
68
+ const e = entry as Record<string, unknown>;
69
+
70
+ if (e.enabled === false) continue;
71
+
72
+ if (e.type === "local" && Array.isArray(e.command) && e.command.length > 0) {
73
+ configs.push({
74
+ name,
75
+ type: "local",
76
+ command: e.command as string[],
77
+ environment: isStringRecord(e.environment) ? e.environment : undefined,
78
+ timeout: typeof e.timeout === "number" ? e.timeout : undefined,
79
+ });
80
+ } else if (e.type === "remote" && typeof e.url === "string") {
81
+ configs.push({
82
+ name,
83
+ type: "remote",
84
+ url: e.url,
85
+ headers: isStringRecord(e.headers) ? e.headers : undefined,
86
+ timeout: typeof e.timeout === "number" ? e.timeout : undefined,
87
+ });
88
+ } else {
89
+ log.debug("Skipping unrecognised MCP config entry", { name, type: e.type });
90
+ }
91
+ }
92
+
93
+ return configs;
94
+ }
95
+
96
+ function isStringRecord(v: unknown): v is Record<string, string> {
97
+ return typeof v === "object" && v !== null && !Array.isArray(v);
98
+ }
@@ -0,0 +1,131 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { createLogger } from "../utils/logger.js";
3
+ import type { McpClientManager } from "./client-manager.js";
4
+
5
+ const log = createLogger("mcp:tool-bridge");
6
+
7
+ interface DiscoveredMcpTool {
8
+ name: string;
9
+ serverName: string;
10
+ description?: string;
11
+ inputSchema?: Record<string, unknown>;
12
+ }
13
+
14
+ /**
15
+ * Build plugin `tool()` hook entries for discovered MCP tools.
16
+ *
17
+ * Each MCP tool is namespaced as `mcp__<server_name>__<tool_name>`
18
+ * to avoid collision with local tools and to make the source clear.
19
+ */
20
+ export function buildMcpToolHookEntries(
21
+ tools: DiscoveredMcpTool[],
22
+ manager: McpClientManager,
23
+ ): Record<string, any> {
24
+ const z = tool.schema;
25
+ const entries: Record<string, any> = {};
26
+
27
+ for (const t of tools) {
28
+ const hookName = namespaceMcpTool(t.serverName, t.name);
29
+
30
+ if (entries[hookName]) {
31
+ log.debug("Duplicate MCP tool name, skipping", { hookName });
32
+ continue;
33
+ }
34
+
35
+ const zodArgs = mcpSchemaToZod(t.inputSchema, z);
36
+ const serverName = t.serverName;
37
+ const toolName = t.name;
38
+
39
+ entries[hookName] = tool({
40
+ description: t.description || `MCP tool: ${t.name} (server: ${t.serverName})`,
41
+ args: zodArgs,
42
+ async execute(args: any) {
43
+ log.debug("Executing MCP tool", { server: serverName, tool: toolName });
44
+ const result = await manager.callTool(serverName, toolName, args ?? {});
45
+ if (result.startsWith("Error:")) {
46
+ throw new Error(result);
47
+ }
48
+ return result;
49
+ },
50
+ });
51
+ }
52
+
53
+ log.debug("Built MCP tool hook entries", { count: Object.keys(entries).length });
54
+ return entries;
55
+ }
56
+
57
+ /**
58
+ * Build OpenAI-format tool definitions for discovered MCP tools.
59
+ * These are injected into chat.params so the model sees the tools.
60
+ */
61
+ export function buildMcpToolDefinitions(tools: DiscoveredMcpTool[]): any[] {
62
+ const defs: any[] = [];
63
+
64
+ for (const t of tools) {
65
+ const name = namespaceMcpTool(t.serverName, t.name);
66
+ defs.push({
67
+ type: "function",
68
+ function: {
69
+ name,
70
+ description: t.description || `MCP tool: ${t.name} (server: ${t.serverName})`,
71
+ parameters: t.inputSchema ?? { type: "object", properties: {} },
72
+ },
73
+ });
74
+ }
75
+
76
+ return defs;
77
+ }
78
+
79
+ function namespaceMcpTool(serverName: string, toolName: string): string {
80
+ const sanitizedServer = serverName.replace(/[^a-zA-Z0-9]/g, "_");
81
+ const sanitizedTool = toolName.replace(/[^a-zA-Z0-9]/g, "_");
82
+ return `mcp__${sanitizedServer}__${sanitizedTool}`;
83
+ }
84
+
85
+ function mcpSchemaToZod(inputSchema: Record<string, unknown> | undefined, z: any): any {
86
+ if (!inputSchema || typeof inputSchema !== "object") {
87
+ return {};
88
+ }
89
+
90
+ const properties = (inputSchema.properties ?? {}) as Record<string, any>;
91
+ const required = (inputSchema.required ?? []) as string[];
92
+ const shape: any = {};
93
+
94
+ for (const [key, prop] of Object.entries(properties)) {
95
+ let zodType: any;
96
+
97
+ switch (prop?.type) {
98
+ case "string":
99
+ zodType = z.string();
100
+ break;
101
+ case "number":
102
+ case "integer":
103
+ zodType = z.number();
104
+ break;
105
+ case "boolean":
106
+ zodType = z.boolean();
107
+ break;
108
+ case "array":
109
+ zodType = z.array(z.any());
110
+ break;
111
+ case "object":
112
+ zodType = z.record(z.any());
113
+ break;
114
+ default:
115
+ zodType = z.any();
116
+ break;
117
+ }
118
+
119
+ if (prop?.description) {
120
+ zodType = zodType.describe(prop.description);
121
+ }
122
+
123
+ if (!required.includes(key)) {
124
+ zodType = zodType.optional();
125
+ }
126
+
127
+ shape[key] = zodType;
128
+ }
129
+
130
+ return shape;
131
+ }