@sammorrowdrums/mcpi-ext 0.1.0 → 0.2.0
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/.github/workflows/ci.yml +1 -1
- package/README.md +109 -139
- package/docs/code-mode.md +38 -0
- package/docs/skills.md +57 -0
- package/docs/tool-cli.md +54 -0
- package/images/code-mode-histogram.png +0 -0
- package/images/skills-enabling-mcp-tools.png +0 -0
- package/images/tool-cli-grep.png +0 -0
- package/mise.toml +1 -13
- package/package.json +5 -7
- package/scripts/test-tool-cli-smoke.mjs +40 -9
- package/src/index.ts +24 -3
- package/src/tool-cli/index.ts +2 -2
- package/dist/code-mode/code-mode-integration.test.d.ts +0 -2
- package/dist/code-mode/code-mode-integration.test.d.ts.map +0 -1
- package/dist/code-mode/code-mode-integration.test.js +0 -116
- package/dist/code-mode/code-mode-integration.test.js.map +0 -1
- package/dist/code-mode/eligibility.d.ts +0 -10
- package/dist/code-mode/eligibility.d.ts.map +0 -1
- package/dist/code-mode/eligibility.js +0 -14
- package/dist/code-mode/eligibility.js.map +0 -1
- package/dist/code-mode/eligibility.test.d.ts +0 -2
- package/dist/code-mode/eligibility.test.d.ts.map +0 -1
- package/dist/code-mode/eligibility.test.js +0 -53
- package/dist/code-mode/eligibility.test.js.map +0 -1
- package/dist/code-mode/executor.d.ts +0 -35
- package/dist/code-mode/executor.d.ts.map +0 -1
- package/dist/code-mode/executor.js +0 -120
- package/dist/code-mode/executor.js.map +0 -1
- package/dist/code-mode/executor.test.d.ts +0 -2
- package/dist/code-mode/executor.test.d.ts.map +0 -1
- package/dist/code-mode/executor.test.js +0 -130
- package/dist/code-mode/executor.test.js.map +0 -1
- package/dist/code-mode/index.d.ts +0 -65
- package/dist/code-mode/index.d.ts.map +0 -1
- package/dist/code-mode/index.js +0 -175
- package/dist/code-mode/index.js.map +0 -1
- package/dist/code-mode/tools.d.ts +0 -45
- package/dist/code-mode/tools.d.ts.map +0 -1
- package/dist/code-mode/tools.js +0 -70
- package/dist/code-mode/tools.js.map +0 -1
- package/dist/code-mode/type-hints.d.ts +0 -20
- package/dist/code-mode/type-hints.d.ts.map +0 -1
- package/dist/code-mode/type-hints.js +0 -189
- package/dist/code-mode/type-hints.js.map +0 -1
- package/dist/code-mode/type-hints.test.d.ts +0 -2
- package/dist/code-mode/type-hints.test.d.ts.map +0 -1
- package/dist/code-mode/type-hints.test.js +0 -157
- package/dist/code-mode/type-hints.test.js.map +0 -1
- package/dist/docker-e2e.d.ts +0 -55
- package/dist/docker-e2e.d.ts.map +0 -1
- package/dist/docker-e2e.js +0 -235
- package/dist/docker-e2e.js.map +0 -1
- package/dist/docker-e2e.test.d.ts +0 -2
- package/dist/docker-e2e.test.d.ts.map +0 -1
- package/dist/docker-e2e.test.js +0 -176
- package/dist/docker-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -128
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/mcp/client-manager.d.ts +0 -46
- package/dist/mcp/client-manager.d.ts.map +0 -1
- package/dist/mcp/client-manager.js +0 -148
- package/dist/mcp/client-manager.js.map +0 -1
- package/dist/mcp/client-manager.test.d.ts +0 -2
- package/dist/mcp/client-manager.test.d.ts.map +0 -1
- package/dist/mcp/client-manager.test.js +0 -204
- package/dist/mcp/client-manager.test.js.map +0 -1
- package/dist/mcp/config-loader.d.ts +0 -8
- package/dist/mcp/config-loader.d.ts.map +0 -1
- package/dist/mcp/config-loader.js +0 -43
- package/dist/mcp/config-loader.js.map +0 -1
- package/dist/mcp/config-loader.test.d.ts +0 -2
- package/dist/mcp/config-loader.test.d.ts.map +0 -1
- package/dist/mcp/config-loader.test.js +0 -93
- package/dist/mcp/config-loader.test.js.map +0 -1
- package/dist/mcp/config.d.ts +0 -46
- package/dist/mcp/config.d.ts.map +0 -1
- package/dist/mcp/config.js +0 -28
- package/dist/mcp/config.js.map +0 -1
- package/dist/mcp/index.d.ts +0 -5
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -4
- package/dist/mcp/index.js.map +0 -1
- package/dist/skills/discover.d.ts +0 -10
- package/dist/skills/discover.d.ts.map +0 -1
- package/dist/skills/discover.js +0 -60
- package/dist/skills/discover.js.map +0 -1
- package/dist/skills/format.d.ts +0 -10
- package/dist/skills/format.d.ts.map +0 -1
- package/dist/skills/format.js +0 -38
- package/dist/skills/format.js.map +0 -1
- package/dist/skills/format.test.d.ts +0 -2
- package/dist/skills/format.test.d.ts.map +0 -1
- package/dist/skills/format.test.js +0 -63
- package/dist/skills/format.test.js.map +0 -1
- package/dist/skills/index.d.ts +0 -6
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js +0 -6
- package/dist/skills/index.js.map +0 -1
- package/dist/skills/load-skill-tool.d.ts +0 -40
- package/dist/skills/load-skill-tool.d.ts.map +0 -1
- package/dist/skills/load-skill-tool.js +0 -104
- package/dist/skills/load-skill-tool.js.map +0 -1
- package/dist/skills/mcp-tool-proxy.d.ts +0 -11
- package/dist/skills/mcp-tool-proxy.d.ts.map +0 -1
- package/dist/skills/mcp-tool-proxy.js +0 -116
- package/dist/skills/mcp-tool-proxy.js.map +0 -1
- package/dist/skills/skill-integration.test.d.ts +0 -2
- package/dist/skills/skill-integration.test.d.ts.map +0 -1
- package/dist/skills/skill-integration.test.js +0 -119
- package/dist/skills/skill-integration.test.js.map +0 -1
- package/dist/skills/skill-registry.d.ts +0 -39
- package/dist/skills/skill-registry.d.ts.map +0 -1
- package/dist/skills/skill-registry.js +0 -48
- package/dist/skills/skill-registry.js.map +0 -1
- package/dist/skills/skill-registry.test.d.ts +0 -2
- package/dist/skills/skill-registry.test.d.ts.map +0 -1
- package/dist/skills/skill-registry.test.js +0 -67
- package/dist/skills/skill-registry.test.js.map +0 -1
- package/dist/test-servers/weather-server.d.ts +0 -12
- package/dist/test-servers/weather-server.d.ts.map +0 -1
- package/dist/test-servers/weather-server.js +0 -139
- package/dist/test-servers/weather-server.js.map +0 -1
- package/dist/test-servers/weather-stdio.d.ts +0 -2
- package/dist/test-servers/weather-stdio.d.ts.map +0 -1
- package/dist/test-servers/weather-stdio.js +0 -11
- package/dist/test-servers/weather-stdio.js.map +0 -1
- package/dist/tool-cli/cli.d.ts +0 -3
- package/dist/tool-cli/cli.d.ts.map +0 -1
- package/dist/tool-cli/cli.js +0 -179
- package/dist/tool-cli/cli.js.map +0 -1
- package/dist/tool-cli/constants.d.ts +0 -7
- package/dist/tool-cli/constants.d.ts.map +0 -1
- package/dist/tool-cli/constants.js +0 -15
- package/dist/tool-cli/constants.js.map +0 -1
- package/dist/tool-cli/format.d.ts +0 -8
- package/dist/tool-cli/format.d.ts.map +0 -1
- package/dist/tool-cli/format.js +0 -57
- package/dist/tool-cli/format.js.map +0 -1
- package/dist/tool-cli/format.test.d.ts +0 -2
- package/dist/tool-cli/format.test.d.ts.map +0 -1
- package/dist/tool-cli/format.test.js +0 -30
- package/dist/tool-cli/format.test.js.map +0 -1
- package/dist/tool-cli/index.d.ts +0 -4
- package/dist/tool-cli/index.d.ts.map +0 -1
- package/dist/tool-cli/index.js +0 -4
- package/dist/tool-cli/index.js.map +0 -1
- package/dist/tool-cli/rpc-client.d.ts +0 -6
- package/dist/tool-cli/rpc-client.d.ts.map +0 -1
- package/dist/tool-cli/rpc-client.js +0 -25
- package/dist/tool-cli/rpc-client.js.map +0 -1
- package/dist/tool-cli/rpc-server.d.ts +0 -48
- package/dist/tool-cli/rpc-server.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.js +0 -220
- package/dist/tool-cli/rpc-server.js.map +0 -1
- package/dist/tool-cli/rpc-server.test.d.ts +0 -2
- package/dist/tool-cli/rpc-server.test.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.test.js +0 -154
- package/dist/tool-cli/rpc-server.test.js.map +0 -1
- package/src/tool-cli/cli.ts +0 -232
- package/src/tool-cli/constants.ts +0 -15
- package/src/tool-cli/rpc-client.ts +0 -39
- package/src/tool-cli/rpc-server.test.ts +0 -193
- package/src/tool-cli/rpc-server.ts +0 -287
package/src/tool-cli/cli.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { rpcCall } from "./rpc-client.js";
|
|
5
|
-
|
|
6
|
-
interface ServerInfo {
|
|
7
|
-
name: string;
|
|
8
|
-
toolCount: number;
|
|
9
|
-
examples: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface ToolSummary {
|
|
13
|
-
name: string;
|
|
14
|
-
description: string;
|
|
15
|
-
hasStructuredOutput: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ToolDetails {
|
|
19
|
-
name: string;
|
|
20
|
-
description: string;
|
|
21
|
-
inputSchema: Record<string, unknown>;
|
|
22
|
-
outputSchema?: Record<string, unknown>;
|
|
23
|
-
annotations?: Record<string, unknown>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function main(): Promise<void> {
|
|
27
|
-
const rawArgs = process.argv.slice(2);
|
|
28
|
-
|
|
29
|
-
// Extract --out <file> flag (can appear anywhere)
|
|
30
|
-
let outFile: string | undefined;
|
|
31
|
-
const args: string[] = [];
|
|
32
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
33
|
-
if (rawArgs[i] === "--out" && i + 1 < rawArgs.length) {
|
|
34
|
-
outFile = rawArgs[++i];
|
|
35
|
-
} else {
|
|
36
|
-
args.push(rawArgs[i]);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// No args or --help → list servers
|
|
42
|
-
if (args.length === 0 || (args.length === 1 && isHelp(args[0]))) {
|
|
43
|
-
await listServers();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const server = args[0];
|
|
48
|
-
|
|
49
|
-
// <server> --help or just <server> → list tools
|
|
50
|
-
if (args.length === 1 || (args.length === 2 && isHelp(args[1]))) {
|
|
51
|
-
await listTools(server);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const tool = args[1];
|
|
56
|
-
|
|
57
|
-
// <server> <tool> --help or just <server> <tool> → describe tool
|
|
58
|
-
if (args.length === 2 || (args.length === 3 && isHelp(args[2]))) {
|
|
59
|
-
await describeTool(server, tool);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// <server> <tool> <json-args> → call tool
|
|
64
|
-
await callTool(server, tool, args[2], outFile);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
-
if (message.includes("ECONNREFUSED") || message.includes("fetch failed")) {
|
|
68
|
-
console.error("Error: tool-cli server not running. Is mcpi-ext loaded?");
|
|
69
|
-
} else {
|
|
70
|
-
console.error(`Error: ${message}`);
|
|
71
|
-
}
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function listServers(): Promise<void> {
|
|
77
|
-
const result = (await rpcCall("listServers")) as { servers: ServerInfo[] };
|
|
78
|
-
const { servers } = result;
|
|
79
|
-
|
|
80
|
-
if (servers.length === 0) {
|
|
81
|
-
console.log("No MCP servers connected.");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
console.log("Available servers:");
|
|
86
|
-
for (const s of servers) {
|
|
87
|
-
const examples = s.examples.length > 0 ? ` (e.g. ${s.examples.join(", ")})` : "";
|
|
88
|
-
console.log(` ${s.name.padEnd(20)} ${s.toolCount} tool(s)${examples}`);
|
|
89
|
-
}
|
|
90
|
-
console.log("");
|
|
91
|
-
console.log("Use: tool-cli <server> to list tools");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function listTools(server: string): Promise<void> {
|
|
95
|
-
const result = (await rpcCall("listTools", { server })) as {
|
|
96
|
-
server: string;
|
|
97
|
-
tools: ToolSummary[];
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
if (result.tools.length === 0) {
|
|
101
|
-
console.log(`${server} — no tools`);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
console.log(`${server} — ${result.tools.length} tool(s):`);
|
|
106
|
-
for (const t of result.tools) {
|
|
107
|
-
const badge = t.hasStructuredOutput ? " [json]" : "";
|
|
108
|
-
console.log(` ${t.name.padEnd(30)} ${t.description}${badge}`);
|
|
109
|
-
}
|
|
110
|
-
console.log("");
|
|
111
|
-
console.log("Use: tool-cli <server> <tool> for full schema");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function describeTool(server: string, tool: string): Promise<void> {
|
|
115
|
-
const result = (await rpcCall("describeTool", { server, tool })) as ToolDetails;
|
|
116
|
-
|
|
117
|
-
console.log(`${result.name} — ${result.description}`);
|
|
118
|
-
console.log("");
|
|
119
|
-
console.log("Input schema:");
|
|
120
|
-
console.log(formatSchema(result.inputSchema));
|
|
121
|
-
|
|
122
|
-
if (result.outputSchema) {
|
|
123
|
-
console.log("");
|
|
124
|
-
console.log("Output schema (structured JSON output):");
|
|
125
|
-
console.log(formatSchema(result.outputSchema));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (result.annotations && Object.keys(result.annotations).length > 0) {
|
|
129
|
-
console.log("");
|
|
130
|
-
console.log("Annotations:");
|
|
131
|
-
for (const [key, val] of Object.entries(result.annotations)) {
|
|
132
|
-
console.log(` ${key}: ${JSON.stringify(val)}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
console.log("");
|
|
137
|
-
console.log(`Use: tool-cli ${server} ${tool} '{"key":"value"}' to call`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function callTool(
|
|
141
|
-
server: string,
|
|
142
|
-
tool: string,
|
|
143
|
-
argsJson: string,
|
|
144
|
-
outFile?: string,
|
|
145
|
-
): Promise<void> {
|
|
146
|
-
let toolArgs: Record<string, unknown>;
|
|
147
|
-
try {
|
|
148
|
-
toolArgs = JSON.parse(argsJson) as Record<string, unknown>;
|
|
149
|
-
} catch {
|
|
150
|
-
console.error(`Error: invalid JSON arguments: ${argsJson}`);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const result = (await rpcCall("callTool", { server, tool, arguments: toolArgs })) as {
|
|
155
|
-
content: unknown[];
|
|
156
|
-
isError?: boolean;
|
|
157
|
-
structuredContent?: Record<string, unknown>;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
if (result.isError) {
|
|
161
|
-
// Error content goes to stderr so stdout stays clean for piping
|
|
162
|
-
if (result.structuredContent) {
|
|
163
|
-
console.error(JSON.stringify(result.structuredContent, null, 2));
|
|
164
|
-
} else if (result.content) {
|
|
165
|
-
for (const item of result.content) {
|
|
166
|
-
const entry = item as Record<string, unknown>;
|
|
167
|
-
if (entry.type === "text") {
|
|
168
|
-
console.error(entry.text);
|
|
169
|
-
} else {
|
|
170
|
-
console.error(JSON.stringify(entry, null, 2));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
process.exit(1);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const output = formatOutput(result.structuredContent, result.content);
|
|
179
|
-
if (!output) return;
|
|
180
|
-
|
|
181
|
-
if (outFile) {
|
|
182
|
-
writeFileSync(outFile, output, "utf-8");
|
|
183
|
-
console.log(`Written to: ${outFile}`);
|
|
184
|
-
} else {
|
|
185
|
-
console.log(output);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Build the output string from structured or raw content. */
|
|
190
|
-
function formatOutput(structuredContent?: Record<string, unknown>, content?: unknown[]): string {
|
|
191
|
-
if (structuredContent) {
|
|
192
|
-
return JSON.stringify(structuredContent, null, 2);
|
|
193
|
-
}
|
|
194
|
-
if (content) {
|
|
195
|
-
const parts: string[] = [];
|
|
196
|
-
for (const item of content) {
|
|
197
|
-
const entry = item as Record<string, unknown>;
|
|
198
|
-
if (entry.type === "text") {
|
|
199
|
-
parts.push(entry.text as string);
|
|
200
|
-
} else {
|
|
201
|
-
parts.push(JSON.stringify(entry, null, 2));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return parts.join("\n");
|
|
205
|
-
}
|
|
206
|
-
return "";
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Format a JSON Schema as a compact, readable summary. */
|
|
210
|
-
function formatSchema(schema: Record<string, unknown>): string {
|
|
211
|
-
const props = schema.properties as Record<string, Record<string, unknown>> | undefined;
|
|
212
|
-
const required = (schema.required as string[]) ?? [];
|
|
213
|
-
|
|
214
|
-
if (!props || Object.keys(props).length === 0) {
|
|
215
|
-
return " (no parameters)";
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const lines: string[] = [];
|
|
219
|
-
for (const [name, prop] of Object.entries(props)) {
|
|
220
|
-
const type = (prop.type as string) ?? "unknown";
|
|
221
|
-
const req = required.includes(name) ? ", required" : "";
|
|
222
|
-
const desc = prop.description ? `: ${prop.description as string}` : "";
|
|
223
|
-
lines.push(` ${name} (${type}${req})${desc}`);
|
|
224
|
-
}
|
|
225
|
-
return lines.join("\n");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function isHelp(arg: string): boolean {
|
|
229
|
-
return arg === "--help" || arg === "-h";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
void main();
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/** Default port for the tool-cli JSON-RPC server. */
|
|
2
|
-
export const DEFAULT_PORT = 7179;
|
|
3
|
-
|
|
4
|
-
/** Environment variable to override the default port. */
|
|
5
|
-
export const PORT_ENV_VAR = "TOOL_CLI_PORT";
|
|
6
|
-
|
|
7
|
-
/** Resolve the port from environment or default. */
|
|
8
|
-
export function resolvePort(): number {
|
|
9
|
-
const envPort = process.env[PORT_ENV_VAR];
|
|
10
|
-
if (envPort) {
|
|
11
|
-
const parsed = parseInt(envPort, 10);
|
|
12
|
-
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) return parsed;
|
|
13
|
-
}
|
|
14
|
-
return DEFAULT_PORT;
|
|
15
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { resolvePort } from "./constants.js";
|
|
2
|
-
|
|
3
|
-
interface JsonRpcResponse {
|
|
4
|
-
jsonrpc: "2.0";
|
|
5
|
-
result?: unknown;
|
|
6
|
-
error?: { code: number; message: string; data?: unknown };
|
|
7
|
-
id: number | string | null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Send a JSON-RPC 2.0 request to the tool-cli RPC server.
|
|
12
|
-
* Throws on network errors and JSON-RPC error responses.
|
|
13
|
-
*/
|
|
14
|
-
export async function rpcCall(
|
|
15
|
-
method: string,
|
|
16
|
-
params: Record<string, unknown> = {},
|
|
17
|
-
): Promise<unknown> {
|
|
18
|
-
const port = resolvePort();
|
|
19
|
-
const url = `http://127.0.0.1:${port}`;
|
|
20
|
-
|
|
21
|
-
const response = await fetch(url, {
|
|
22
|
-
method: "POST",
|
|
23
|
-
headers: { "Content-Type": "application/json" },
|
|
24
|
-
body: JSON.stringify({
|
|
25
|
-
jsonrpc: "2.0",
|
|
26
|
-
method,
|
|
27
|
-
params,
|
|
28
|
-
id: 1,
|
|
29
|
-
}),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const json = (await response.json()) as JsonRpcResponse;
|
|
33
|
-
|
|
34
|
-
if (json.error) {
|
|
35
|
-
throw new Error(json.error.message);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return json.result;
|
|
39
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { ToolCliRpcServer } from "./rpc-server.js";
|
|
3
|
-
import type { McpClientManager } from "../mcp/index.js";
|
|
4
|
-
|
|
5
|
-
/** Minimal mock of McpClientManager for RPC server testing. */
|
|
6
|
-
function createMockManager(): McpClientManager {
|
|
7
|
-
const tools = [
|
|
8
|
-
{
|
|
9
|
-
name: "get_weather",
|
|
10
|
-
description: "Get current weather for a city",
|
|
11
|
-
inputSchema: {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: { city: { type: "string", description: "City name" } },
|
|
14
|
-
required: ["city"],
|
|
15
|
-
},
|
|
16
|
-
annotations: { readOnlyHint: true },
|
|
17
|
-
serverName: "weather",
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "set_alarm",
|
|
21
|
-
description: "Set a weather alarm",
|
|
22
|
-
inputSchema: {
|
|
23
|
-
type: "object",
|
|
24
|
-
properties: {
|
|
25
|
-
city: { type: "string" },
|
|
26
|
-
threshold: { type: "number" },
|
|
27
|
-
},
|
|
28
|
-
required: ["city", "threshold"],
|
|
29
|
-
},
|
|
30
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
31
|
-
serverName: "weather",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "search_docs",
|
|
35
|
-
description: "Search documentation",
|
|
36
|
-
inputSchema: {
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: { query: { type: "string", description: "Search query" } },
|
|
39
|
-
required: ["query"],
|
|
40
|
-
},
|
|
41
|
-
outputSchema: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: { results: { type: "array" } },
|
|
44
|
-
},
|
|
45
|
-
annotations: { readOnlyHint: true },
|
|
46
|
-
serverName: "docs",
|
|
47
|
-
},
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
getConnectedServers: () => ["weather", "docs"],
|
|
52
|
-
getToolsForServer: (name: string) => tools.filter((t) => t.serverName === name),
|
|
53
|
-
getTools: () => tools,
|
|
54
|
-
getClient: (name: string) => {
|
|
55
|
-
if (name === "weather" || name === "docs") {
|
|
56
|
-
return {
|
|
57
|
-
callTool: async (params: { name: string; arguments?: Record<string, unknown> }) => ({
|
|
58
|
-
content: [{ type: "text", text: `Called ${params.name}` }],
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
},
|
|
64
|
-
} as unknown as McpClientManager;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Use port 0 so the OS assigns a free port — avoids collisions when src + dist tests run together
|
|
68
|
-
describe("ToolCliRpcServer", () => {
|
|
69
|
-
let server: ToolCliRpcServer;
|
|
70
|
-
let port: number;
|
|
71
|
-
|
|
72
|
-
beforeAll(async () => {
|
|
73
|
-
const manager = createMockManager();
|
|
74
|
-
server = new ToolCliRpcServer(manager, 0);
|
|
75
|
-
await server.start();
|
|
76
|
-
port = server.getPort();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterAll(async () => {
|
|
80
|
-
await server.stop();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
async function rpc(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
84
|
-
const res = await fetch(`http://127.0.0.1:${port}`, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: { "Content-Type": "application/json" },
|
|
87
|
-
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
|
|
88
|
-
});
|
|
89
|
-
return res.json();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
it("lists connected servers with tool counts", async () => {
|
|
93
|
-
const resp = (await rpc("listServers")) as {
|
|
94
|
-
result: { servers: { name: string; toolCount: number; examples: string[] }[] };
|
|
95
|
-
};
|
|
96
|
-
const { servers } = resp.result;
|
|
97
|
-
expect(servers).toHaveLength(2);
|
|
98
|
-
|
|
99
|
-
const weather = servers.find((s) => s.name === "weather");
|
|
100
|
-
expect(weather).toBeDefined();
|
|
101
|
-
expect(weather?.toolCount).toBe(2);
|
|
102
|
-
expect(weather?.examples.length).toBeGreaterThan(0);
|
|
103
|
-
|
|
104
|
-
const docs = servers.find((s) => s.name === "docs");
|
|
105
|
-
expect(docs).toBeDefined();
|
|
106
|
-
expect(docs?.toolCount).toBe(1);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("lists tools for a specific server", async () => {
|
|
110
|
-
const resp = (await rpc("listTools", { server: "weather" })) as {
|
|
111
|
-
result: { server: string; tools: { name: string; description: string }[] };
|
|
112
|
-
};
|
|
113
|
-
expect(resp.result.server).toBe("weather");
|
|
114
|
-
expect(resp.result.tools).toHaveLength(2);
|
|
115
|
-
expect(resp.result.tools[0].name).toBe("get_weather");
|
|
116
|
-
expect(resp.result.tools[1].name).toBe("set_alarm");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("describes a tool with full schema", async () => {
|
|
120
|
-
const resp = (await rpc("describeTool", { server: "weather", tool: "get_weather" })) as {
|
|
121
|
-
result: {
|
|
122
|
-
name: string;
|
|
123
|
-
description: string;
|
|
124
|
-
inputSchema: Record<string, unknown>;
|
|
125
|
-
annotations: Record<string, unknown>;
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
expect(resp.result.name).toBe("get_weather");
|
|
129
|
-
expect(resp.result.description).toBe("Get current weather for a city");
|
|
130
|
-
expect(resp.result.inputSchema).toHaveProperty("properties");
|
|
131
|
-
expect(resp.result.annotations).toEqual({ readOnlyHint: true });
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("calls a tool and returns result", async () => {
|
|
135
|
-
const resp = (await rpc("callTool", {
|
|
136
|
-
server: "weather",
|
|
137
|
-
tool: "get_weather",
|
|
138
|
-
arguments: { city: "London" },
|
|
139
|
-
})) as {
|
|
140
|
-
result: { content: { type: string; text: string }[] };
|
|
141
|
-
};
|
|
142
|
-
expect(resp.result.content).toHaveLength(1);
|
|
143
|
-
expect(resp.result.content[0].text).toBe("Called get_weather");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("returns error for unknown server", async () => {
|
|
147
|
-
const resp = (await rpc("listTools", { server: "nonexistent" })) as {
|
|
148
|
-
error: { code: number; message: string };
|
|
149
|
-
};
|
|
150
|
-
expect(resp.error).toBeDefined();
|
|
151
|
-
expect(resp.error.code).toBe(-32602);
|
|
152
|
-
expect(resp.error.message).toContain("not found");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("returns error for unknown tool", async () => {
|
|
156
|
-
const resp = (await rpc("describeTool", { server: "weather", tool: "nonexistent" })) as {
|
|
157
|
-
error: { code: number; message: string };
|
|
158
|
-
};
|
|
159
|
-
expect(resp.error).toBeDefined();
|
|
160
|
-
expect(resp.error.message).toContain("not found");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("returns error for unknown method", async () => {
|
|
164
|
-
const resp = (await rpc("unknownMethod")) as {
|
|
165
|
-
error: { code: number; message: string };
|
|
166
|
-
};
|
|
167
|
-
expect(resp.error).toBeDefined();
|
|
168
|
-
expect(resp.error.code).toBe(-32601);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("returns error for missing required params", async () => {
|
|
172
|
-
const resp = (await rpc("listTools", {})) as {
|
|
173
|
-
error: { code: number; message: string };
|
|
174
|
-
};
|
|
175
|
-
expect(resp.error).toBeDefined();
|
|
176
|
-
expect(resp.error.code).toBe(-32602);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("rejects non-POST requests", async () => {
|
|
180
|
-
const res = await fetch(`http://127.0.0.1:${port}`, { method: "GET" });
|
|
181
|
-
expect(res.status).toBe(405);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("handles malformed JSON gracefully", async () => {
|
|
185
|
-
const res = await fetch(`http://127.0.0.1:${port}`, {
|
|
186
|
-
method: "POST",
|
|
187
|
-
headers: { "Content-Type": "application/json" },
|
|
188
|
-
body: "not json at all",
|
|
189
|
-
});
|
|
190
|
-
const json = (await res.json()) as { error: { code: number } };
|
|
191
|
-
expect(json.error.code).toBe(-32700);
|
|
192
|
-
});
|
|
193
|
-
});
|