@sammorrowdrums/mcpi-ext 0.2.0 → 0.2.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/dist/code-mode/code-mode-integration.test.d.ts +2 -0
- package/dist/code-mode/code-mode-integration.test.d.ts.map +1 -0
- package/dist/code-mode/code-mode-integration.test.js +116 -0
- package/dist/code-mode/code-mode-integration.test.js.map +1 -0
- package/dist/code-mode/eligibility.d.ts +10 -0
- package/dist/code-mode/eligibility.d.ts.map +1 -0
- package/dist/code-mode/eligibility.js +14 -0
- package/dist/code-mode/eligibility.js.map +1 -0
- package/dist/code-mode/eligibility.test.d.ts +2 -0
- package/dist/code-mode/eligibility.test.d.ts.map +1 -0
- package/dist/code-mode/eligibility.test.js +53 -0
- package/dist/code-mode/eligibility.test.js.map +1 -0
- package/dist/code-mode/executor.d.ts +35 -0
- package/dist/code-mode/executor.d.ts.map +1 -0
- package/dist/code-mode/executor.js +120 -0
- package/dist/code-mode/executor.js.map +1 -0
- package/dist/code-mode/executor.test.d.ts +2 -0
- package/dist/code-mode/executor.test.d.ts.map +1 -0
- package/dist/code-mode/executor.test.js +130 -0
- package/dist/code-mode/executor.test.js.map +1 -0
- package/dist/code-mode/index.d.ts +65 -0
- package/dist/code-mode/index.d.ts.map +1 -0
- package/dist/code-mode/index.js +175 -0
- package/dist/code-mode/index.js.map +1 -0
- package/dist/code-mode/tools.d.ts +45 -0
- package/dist/code-mode/tools.d.ts.map +1 -0
- package/dist/code-mode/tools.js +70 -0
- package/dist/code-mode/tools.js.map +1 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.d.ts.map +1 -0
- package/dist/code-mode/type-hints.js +189 -0
- package/dist/code-mode/type-hints.js.map +1 -0
- package/dist/code-mode/type-hints.test.d.ts +2 -0
- package/dist/code-mode/type-hints.test.d.ts.map +1 -0
- package/dist/code-mode/type-hints.test.js +157 -0
- package/dist/code-mode/type-hints.test.js.map +1 -0
- package/dist/docker-e2e.d.ts +55 -0
- package/dist/docker-e2e.d.ts.map +1 -0
- package/dist/docker-e2e.js +235 -0
- package/dist/docker-e2e.js.map +1 -0
- package/dist/docker-e2e.test.d.ts +2 -0
- package/dist/docker-e2e.test.d.ts.map +1 -0
- package/dist/docker-e2e.test.js +176 -0
- package/dist/docker-e2e.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +8 -0
- package/dist/index.test.js.map +1 -0
- package/dist/mcp/client-manager.d.ts +46 -0
- package/dist/mcp/client-manager.d.ts.map +1 -0
- package/dist/mcp/client-manager.js +148 -0
- package/dist/mcp/client-manager.js.map +1 -0
- package/dist/mcp/client-manager.test.d.ts +2 -0
- package/dist/mcp/client-manager.test.d.ts.map +1 -0
- package/dist/mcp/client-manager.test.js +204 -0
- package/dist/mcp/client-manager.test.js.map +1 -0
- package/dist/mcp/config-loader.d.ts +8 -0
- package/dist/mcp/config-loader.d.ts.map +1 -0
- package/dist/mcp/config-loader.js +43 -0
- package/dist/mcp/config-loader.js.map +1 -0
- package/dist/mcp/config-loader.test.d.ts +2 -0
- package/dist/mcp/config-loader.test.d.ts.map +1 -0
- package/dist/mcp/config-loader.test.js +93 -0
- package/dist/mcp/config-loader.test.js.map +1 -0
- package/dist/mcp/config.d.ts +46 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +28 -0
- package/dist/mcp/config.js.map +1 -0
- package/{src/mcp/index.ts → dist/mcp/index.d.ts} +1 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/skills/discover.d.ts +10 -0
- package/dist/skills/discover.d.ts.map +1 -0
- package/dist/skills/discover.js +60 -0
- package/dist/skills/discover.js.map +1 -0
- package/dist/skills/format.d.ts +10 -0
- package/dist/skills/format.d.ts.map +1 -0
- package/dist/skills/format.js +38 -0
- package/dist/skills/format.js.map +1 -0
- package/dist/skills/format.test.d.ts +2 -0
- package/dist/skills/format.test.d.ts.map +1 -0
- package/dist/skills/format.test.js +63 -0
- package/dist/skills/format.test.js.map +1 -0
- package/{src/skills/index.ts → dist/skills/index.d.ts} +2 -5
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/load-skill-tool.d.ts +40 -0
- package/dist/skills/load-skill-tool.d.ts.map +1 -0
- package/dist/skills/load-skill-tool.js +104 -0
- package/dist/skills/load-skill-tool.js.map +1 -0
- package/dist/skills/mcp-tool-proxy.d.ts +11 -0
- package/dist/skills/mcp-tool-proxy.d.ts.map +1 -0
- package/dist/skills/mcp-tool-proxy.js +116 -0
- package/dist/skills/mcp-tool-proxy.js.map +1 -0
- package/dist/skills/skill-integration.test.d.ts +2 -0
- package/dist/skills/skill-integration.test.d.ts.map +1 -0
- package/dist/skills/skill-integration.test.js +119 -0
- package/dist/skills/skill-integration.test.js.map +1 -0
- package/dist/skills/skill-registry.d.ts +39 -0
- package/dist/skills/skill-registry.d.ts.map +1 -0
- package/dist/skills/skill-registry.js +48 -0
- package/dist/skills/skill-registry.js.map +1 -0
- package/dist/skills/skill-registry.test.d.ts +2 -0
- package/dist/skills/skill-registry.test.d.ts.map +1 -0
- package/dist/skills/skill-registry.test.js +67 -0
- package/dist/skills/skill-registry.test.js.map +1 -0
- package/dist/test-servers/weather-server.d.ts +12 -0
- package/dist/test-servers/weather-server.d.ts.map +1 -0
- package/dist/test-servers/weather-server.js +139 -0
- package/dist/test-servers/weather-server.js.map +1 -0
- package/dist/test-servers/weather-stdio.d.ts +2 -0
- package/dist/test-servers/weather-stdio.d.ts.map +1 -0
- package/{src/test-servers/weather-stdio.ts → dist/test-servers/weather-stdio.js} +1 -1
- package/dist/test-servers/weather-stdio.js.map +1 -0
- package/dist/tool-cli/cli.d.ts +3 -0
- package/dist/tool-cli/cli.d.ts.map +1 -0
- package/dist/tool-cli/cli.js +179 -0
- package/dist/tool-cli/cli.js.map +1 -0
- package/dist/tool-cli/constants.d.ts +7 -0
- package/dist/tool-cli/constants.d.ts.map +1 -0
- package/dist/tool-cli/constants.js +15 -0
- package/dist/tool-cli/constants.js.map +1 -0
- package/dist/tool-cli/format.d.ts +8 -0
- package/dist/tool-cli/format.d.ts.map +1 -0
- package/{src/tool-cli/format.ts → dist/tool-cli/format.js} +5 -4
- package/dist/tool-cli/format.js.map +1 -0
- package/dist/tool-cli/format.test.d.ts +2 -0
- package/dist/tool-cli/format.test.d.ts.map +1 -0
- package/dist/tool-cli/format.test.js +30 -0
- package/dist/tool-cli/format.test.js.map +1 -0
- package/{src/tool-cli/index.ts → dist/tool-cli/index.d.ts} +1 -0
- package/dist/tool-cli/index.d.ts.map +1 -0
- package/dist/tool-cli/index.js +3 -0
- package/dist/tool-cli/index.js.map +1 -0
- package/dist/tool-cli/rpc-client.d.ts +6 -0
- package/dist/tool-cli/rpc-client.d.ts.map +1 -0
- package/dist/tool-cli/rpc-client.js +25 -0
- package/dist/tool-cli/rpc-client.js.map +1 -0
- package/dist/tool-cli/rpc-server.d.ts +48 -0
- package/dist/tool-cli/rpc-server.d.ts.map +1 -0
- package/dist/tool-cli/rpc-server.js +220 -0
- package/dist/tool-cli/rpc-server.js.map +1 -0
- package/dist/tool-cli/rpc-server.test.d.ts +2 -0
- package/dist/tool-cli/rpc-server.test.d.ts.map +1 -0
- package/dist/tool-cli/rpc-server.test.js +154 -0
- package/dist/tool-cli/rpc-server.test.js.map +1 -0
- package/package.json +5 -1
- package/.dockerignore +0 -6
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/ci.yml +0 -50
- package/.hadolint.yaml +0 -6
- package/.prettierignore +0 -4
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -118
- package/DECISIONS.md +0 -99
- package/Dockerfile +0 -39
- package/aube-lock.yaml +0 -4146
- package/docs/code-mode.md +0 -38
- package/docs/skills.md +0 -57
- package/docs/tool-cli.md +0 -54
- package/eslint.config.js +0 -27
- package/images/banner.webp +0 -0
- package/images/code-c-maude.webp +0 -0
- package/images/code-mode-histogram.png +0 -0
- package/images/nuclear-mcp-football.webp +0 -0
- package/images/skills-enabling-mcp-tools.png +0 -0
- package/images/the-skill-dealer.webp +0 -0
- package/images/tool-cli-grep.png +0 -0
- package/mise.toml +0 -53
- package/scripts/docker-entrypoint.sh +0 -29
- package/scripts/test-echo-server.mjs +0 -28
- package/scripts/test-mcp-integration.mjs +0 -59
- package/scripts/test-tool-cli-smoke.mjs +0 -93
- package/src/code-mode/code-mode-integration.test.ts +0 -135
- package/src/code-mode/eligibility.test.ts +0 -60
- package/src/code-mode/eligibility.ts +0 -16
- package/src/code-mode/executor.test.ts +0 -168
- package/src/code-mode/executor.ts +0 -163
- package/src/code-mode/index.ts +0 -208
- package/src/code-mode/tools.ts +0 -110
- package/src/code-mode/type-hints.test.ts +0 -187
- package/src/code-mode/type-hints.ts +0 -249
- package/src/docker-e2e.test.ts +0 -195
- package/src/docker-e2e.ts +0 -277
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -179
- package/src/mcp/client-manager.test.ts +0 -250
- package/src/mcp/client-manager.ts +0 -199
- package/src/mcp/config-loader.test.ts +0 -115
- package/src/mcp/config-loader.ts +0 -46
- package/src/mcp/config.ts +0 -40
- package/src/skills/discover.ts +0 -81
- package/src/skills/format.test.ts +0 -70
- package/src/skills/format.ts +0 -42
- package/src/skills/load-skill-tool.ts +0 -136
- package/src/skills/mcp-tool-proxy.ts +0 -143
- package/src/skills/skill-integration.test.ts +0 -139
- package/src/skills/skill-registry.test.ts +0 -83
- package/src/skills/skill-registry.ts +0 -69
- package/src/test-servers/weather-server.ts +0 -171
- package/src/tool-cli/format.test.ts +0 -33
- package/tsconfig.json +0 -21
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { McpClientManager } from "./client-manager.js";
|
|
3
|
-
|
|
4
|
-
// Mock the MCP SDK transports and client
|
|
5
|
-
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => {
|
|
6
|
-
class MockClient {
|
|
7
|
-
private _onToolsChanged?: (error: Error | null, tools: unknown[] | null) => void;
|
|
8
|
-
|
|
9
|
-
constructor(
|
|
10
|
-
_info: unknown,
|
|
11
|
-
options?: {
|
|
12
|
-
listChanged?: {
|
|
13
|
-
tools?: { onChanged?: (error: Error | null, tools: unknown[] | null) => void };
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
) {
|
|
17
|
-
this._onToolsChanged = options?.listChanged?.tools?.onChanged;
|
|
18
|
-
}
|
|
19
|
-
async connect(_transport: unknown): Promise<void> {
|
|
20
|
-
// no-op for tests
|
|
21
|
-
}
|
|
22
|
-
async close(): Promise<void> {
|
|
23
|
-
// no-op for tests
|
|
24
|
-
}
|
|
25
|
-
async listTools(): Promise<{ tools: unknown[] }> {
|
|
26
|
-
return {
|
|
27
|
-
tools: [
|
|
28
|
-
{
|
|
29
|
-
name: "mock_tool",
|
|
30
|
-
description: "A mock tool for testing",
|
|
31
|
-
inputSchema: { type: "object", properties: { arg: { type: "string" } } },
|
|
32
|
-
annotations: { readOnlyHint: true },
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "mock_tool_2",
|
|
36
|
-
description: "Another mock tool",
|
|
37
|
-
inputSchema: { type: "object", properties: {} },
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Test helper to simulate tools/list_changed notification. */
|
|
44
|
-
_simulateToolsChanged(tools: unknown[]): void {
|
|
45
|
-
this._onToolsChanged?.(null, tools);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { Client: MockClient };
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
53
|
-
StdioClientTransport: class {
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
55
|
-
constructor(_params: unknown) {
|
|
56
|
-
// no-op mock
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
63
|
-
StreamableHTTPClientTransport: class {
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
65
|
-
constructor(_url: URL, _opts?: unknown) {
|
|
66
|
-
// no-op mock
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
describe("McpClientManager", () => {
|
|
72
|
-
let manager: McpClientManager;
|
|
73
|
-
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
manager = new McpClientManager();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("connects to a stdio server and discovers tools", async () => {
|
|
79
|
-
await manager.connectAll({
|
|
80
|
-
mcpServers: {
|
|
81
|
-
"test-stdio": {
|
|
82
|
-
type: "stdio",
|
|
83
|
-
command: "node",
|
|
84
|
-
args: ["test-server.js"],
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(manager.getConnectedServers()).toEqual(["test-stdio"]);
|
|
90
|
-
|
|
91
|
-
const tools = manager.getTools();
|
|
92
|
-
expect(tools).toHaveLength(2);
|
|
93
|
-
expect(tools[0].name).toBe("mock_tool");
|
|
94
|
-
expect(tools[0].serverName).toBe("test-stdio");
|
|
95
|
-
expect(tools[0].description).toBe("A mock tool for testing");
|
|
96
|
-
expect(tools[0].annotations).toEqual({ readOnlyHint: true });
|
|
97
|
-
expect(tools[1].name).toBe("mock_tool_2");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("connects to a remote server and discovers tools", async () => {
|
|
101
|
-
await manager.connectAll({
|
|
102
|
-
mcpServers: {
|
|
103
|
-
"test-remote": {
|
|
104
|
-
type: "remote",
|
|
105
|
-
url: "https://example.com/mcp",
|
|
106
|
-
headers: { Authorization: "Bearer test" },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(manager.getConnectedServers()).toEqual(["test-remote"]);
|
|
112
|
-
expect(manager.getTools()).toHaveLength(2);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("connects to multiple servers and aggregates tools", async () => {
|
|
116
|
-
await manager.connectAll({
|
|
117
|
-
mcpServers: {
|
|
118
|
-
server1: { type: "stdio", command: "node", args: ["s1.js"] },
|
|
119
|
-
server2: { type: "remote", url: "https://example.com/mcp" },
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
expect(manager.getConnectedServers()).toHaveLength(2);
|
|
124
|
-
// 2 tools per server
|
|
125
|
-
expect(manager.getTools()).toHaveLength(4);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("getToolsForServer returns tools for a specific server", async () => {
|
|
129
|
-
await manager.connectAll({
|
|
130
|
-
mcpServers: {
|
|
131
|
-
srv: { type: "stdio", command: "echo" },
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(manager.getToolsForServer("srv")).toHaveLength(2);
|
|
136
|
-
expect(manager.getToolsForServer("nonexistent")).toEqual([]);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("getClient returns the client for a connected server", async () => {
|
|
140
|
-
await manager.connectAll({
|
|
141
|
-
mcpServers: {
|
|
142
|
-
srv: { type: "stdio", command: "echo" },
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(manager.getClient("srv")).toBeDefined();
|
|
147
|
-
expect(manager.getClient("nonexistent")).toBeUndefined();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("disconnectOne removes a server", async () => {
|
|
151
|
-
await manager.connectAll({
|
|
152
|
-
mcpServers: {
|
|
153
|
-
srv: { type: "stdio", command: "echo" },
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
expect(manager.getConnectedServers()).toEqual(["srv"]);
|
|
158
|
-
await manager.disconnectOne("srv");
|
|
159
|
-
expect(manager.getConnectedServers()).toEqual([]);
|
|
160
|
-
expect(manager.getTools()).toEqual([]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("disconnectAll removes all servers", async () => {
|
|
164
|
-
await manager.connectAll({
|
|
165
|
-
mcpServers: {
|
|
166
|
-
s1: { type: "stdio", command: "echo" },
|
|
167
|
-
s2: { type: "remote", url: "https://example.com/mcp" },
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
expect(manager.getConnectedServers()).toHaveLength(2);
|
|
172
|
-
await manager.disconnectAll();
|
|
173
|
-
expect(manager.getConnectedServers()).toEqual([]);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("handles empty config gracefully", async () => {
|
|
177
|
-
await manager.connectAll({ mcpServers: {} });
|
|
178
|
-
expect(manager.getConnectedServers()).toEqual([]);
|
|
179
|
-
expect(manager.getTools()).toEqual([]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("reconnects when connecting to an already-connected server name", async () => {
|
|
183
|
-
await manager.connectOne("srv", { type: "stdio", command: "node" });
|
|
184
|
-
expect(manager.getConnectedServers()).toEqual(["srv"]);
|
|
185
|
-
|
|
186
|
-
// Connect again with same name — should replace
|
|
187
|
-
await manager.connectOne("srv", { type: "remote", url: "https://example.com/mcp" });
|
|
188
|
-
expect(manager.getConnectedServers()).toEqual(["srv"]);
|
|
189
|
-
expect(manager.getTools()).toHaveLength(2);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("logs failures but continues connecting other servers", async () => {
|
|
193
|
-
const logs: string[] = [];
|
|
194
|
-
// Patch Client.connect to fail for a specific transport
|
|
195
|
-
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
196
|
-
const origConnect = Client.prototype.connect;
|
|
197
|
-
let callCount = 0;
|
|
198
|
-
Client.prototype.connect = async function (transport: unknown) {
|
|
199
|
-
callCount++;
|
|
200
|
-
if (callCount === 1) throw new Error("Connection refused");
|
|
201
|
-
return origConnect.call(
|
|
202
|
-
this,
|
|
203
|
-
transport as import("@modelcontextprotocol/sdk/shared/transport.js").Transport,
|
|
204
|
-
);
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
await manager.connectAll(
|
|
209
|
-
{
|
|
210
|
-
mcpServers: {
|
|
211
|
-
failing: { type: "stdio", command: "bad-command" },
|
|
212
|
-
working: { type: "stdio", command: "good-command" },
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
(msg) => logs.push(msg),
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// The working server should still be connected
|
|
219
|
-
expect(manager.getConnectedServers()).toContain("working");
|
|
220
|
-
expect(logs.some((l) => l.includes("Failed to connect") && l.includes("failing"))).toBe(true);
|
|
221
|
-
} finally {
|
|
222
|
-
Client.prototype.connect = origConnect;
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("updates tools when list_changed notification fires", async () => {
|
|
227
|
-
const logs: string[] = [];
|
|
228
|
-
await manager.connectAll({ mcpServers: { srv: { type: "stdio", command: "echo" } } }, (msg) =>
|
|
229
|
-
logs.push(msg),
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
expect(manager.getToolsForServer("srv")).toHaveLength(2);
|
|
233
|
-
|
|
234
|
-
// Simulate a tools/list_changed notification via the mock
|
|
235
|
-
const client = manager.getClient("srv") as unknown as {
|
|
236
|
-
_simulateToolsChanged: (tools: unknown[]) => void;
|
|
237
|
-
};
|
|
238
|
-
client._simulateToolsChanged([
|
|
239
|
-
{
|
|
240
|
-
name: "new_tool",
|
|
241
|
-
description: "A dynamically added tool",
|
|
242
|
-
inputSchema: { type: "object", properties: {} },
|
|
243
|
-
},
|
|
244
|
-
]);
|
|
245
|
-
|
|
246
|
-
expect(manager.getToolsForServer("srv")).toHaveLength(1);
|
|
247
|
-
expect(manager.getToolsForServer("srv")[0].name).toBe("new_tool");
|
|
248
|
-
expect(logs.some((l) => l.includes("Tools updated"))).toBe(true);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
-
import type { McpConfig, ServerConfig } from "./config.js";
|
|
5
|
-
|
|
6
|
-
/** An MCP tool as discovered from a server. */
|
|
7
|
-
export interface McpTool {
|
|
8
|
-
/** Tool name as reported by the server. */
|
|
9
|
-
name: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
/** JSON Schema for the tool's parameters. */
|
|
12
|
-
inputSchema: Record<string, unknown>;
|
|
13
|
-
/** JSON Schema for structured output, if declared. */
|
|
14
|
-
outputSchema?: Record<string, unknown>;
|
|
15
|
-
/** Tool annotations (readOnlyHint, destructiveHint, etc.). */
|
|
16
|
-
annotations?: Record<string, unknown>;
|
|
17
|
-
/** Which configured server this tool came from. */
|
|
18
|
-
serverName: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ManagedConnection {
|
|
22
|
-
client: Client;
|
|
23
|
-
transport: StdioClientTransport | StreamableHTTPClientTransport;
|
|
24
|
-
tools: McpTool[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Manages connections to multiple MCP servers and aggregates their tools.
|
|
29
|
-
*
|
|
30
|
-
* Tools are discovered and stored internally. They are NOT registered with pi
|
|
31
|
-
* automatically — that responsibility belongs to the access tiers (Skills,
|
|
32
|
-
* Football, Code Mode).
|
|
33
|
-
*/
|
|
34
|
-
export class McpClientManager {
|
|
35
|
-
private connections = new Map<string, ManagedConnection>();
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Connect to all servers defined in the config.
|
|
39
|
-
* Connections that fail are logged and skipped — partial success is fine.
|
|
40
|
-
*/
|
|
41
|
-
async connectAll(config: McpConfig, log: (msg: string) => void = console.error): Promise<void> {
|
|
42
|
-
const entries = Object.entries(config.mcpServers);
|
|
43
|
-
const results = await Promise.allSettled(
|
|
44
|
-
entries.map(([name, serverConfig]) => this.connectOne(name, serverConfig, log)),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < results.length; i++) {
|
|
48
|
-
const result = results[i];
|
|
49
|
-
if (result.status === "rejected") {
|
|
50
|
-
log(`[mcp] Failed to connect to "${entries[i][0]}": ${result.reason}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Connect to a single server by name. */
|
|
56
|
-
async connectOne(
|
|
57
|
-
name: string,
|
|
58
|
-
serverConfig: ServerConfig,
|
|
59
|
-
log: (msg: string) => void = console.error,
|
|
60
|
-
): Promise<void> {
|
|
61
|
-
// Disconnect existing connection with this name if any
|
|
62
|
-
if (this.connections.has(name)) {
|
|
63
|
-
await this.disconnectOne(name);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const transport = createTransport(serverConfig);
|
|
67
|
-
|
|
68
|
-
const client = new Client(
|
|
69
|
-
{ name: "mcpi-ext", version: "0.1.0" },
|
|
70
|
-
{
|
|
71
|
-
capabilities: {},
|
|
72
|
-
listChanged: {
|
|
73
|
-
tools: {
|
|
74
|
-
onChanged: (_error, tools) => {
|
|
75
|
-
if (tools) {
|
|
76
|
-
const conn = this.connections.get(name);
|
|
77
|
-
if (conn && conn.client === client) {
|
|
78
|
-
conn.tools = toMcpTools(name, tools);
|
|
79
|
-
log(`[mcp] Tools updated for "${name}" (${conn.tools.length} tools)`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// Insert placeholder so listChanged notifications during connect aren't dropped
|
|
89
|
-
this.connections.set(name, { client, transport, tools: [] });
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
await client.connect(transport);
|
|
93
|
-
|
|
94
|
-
const toolsResult = await client.listTools();
|
|
95
|
-
const tools = toMcpTools(name, toolsResult.tools);
|
|
96
|
-
const conn = this.connections.get(name);
|
|
97
|
-
if (conn && conn.client === client) {
|
|
98
|
-
conn.tools = tools;
|
|
99
|
-
}
|
|
100
|
-
log(`[mcp] Connected to "${name}" (${tools.length} tools)`);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
// Clean up on failure so we don't leak a partially connected client
|
|
103
|
-
const conn = this.connections.get(name);
|
|
104
|
-
if (conn && conn.client === client) {
|
|
105
|
-
this.connections.delete(name);
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
await client.close();
|
|
109
|
-
} catch {
|
|
110
|
-
// best-effort cleanup
|
|
111
|
-
}
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Disconnect a single server. */
|
|
117
|
-
async disconnectOne(name: string): Promise<void> {
|
|
118
|
-
const conn = this.connections.get(name);
|
|
119
|
-
if (!conn) return;
|
|
120
|
-
this.connections.delete(name);
|
|
121
|
-
try {
|
|
122
|
-
await conn.client.close();
|
|
123
|
-
} catch {
|
|
124
|
-
// best-effort
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Disconnect all servers. */
|
|
129
|
-
async disconnectAll(): Promise<void> {
|
|
130
|
-
const names = [...this.connections.keys()];
|
|
131
|
-
await Promise.allSettled(names.map((n) => this.disconnectOne(n)));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Get all discovered tools across all connected servers. */
|
|
135
|
-
getTools(): McpTool[] {
|
|
136
|
-
const tools: McpTool[] = [];
|
|
137
|
-
for (const conn of this.connections.values()) {
|
|
138
|
-
tools.push(...conn.tools);
|
|
139
|
-
}
|
|
140
|
-
return tools;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Get tools from a specific server. */
|
|
144
|
-
getToolsForServer(name: string): McpTool[] {
|
|
145
|
-
return this.connections.get(name)?.tools ?? [];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Get the MCP Client for a specific server (needed to call tools). */
|
|
149
|
-
getClient(name: string): Client | undefined {
|
|
150
|
-
return this.connections.get(name)?.client;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** List connected server names. */
|
|
154
|
-
getConnectedServers(): string[] {
|
|
155
|
-
return [...this.connections.keys()];
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function createTransport(
|
|
160
|
-
config: ServerConfig,
|
|
161
|
-
): StdioClientTransport | StreamableHTTPClientTransport {
|
|
162
|
-
if (config.type === "stdio") {
|
|
163
|
-
const env = config.env
|
|
164
|
-
? Object.fromEntries(
|
|
165
|
-
Object.entries({ ...process.env, ...config.env }).filter(
|
|
166
|
-
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
167
|
-
),
|
|
168
|
-
)
|
|
169
|
-
: undefined;
|
|
170
|
-
return new StdioClientTransport({
|
|
171
|
-
command: config.command,
|
|
172
|
-
args: config.args,
|
|
173
|
-
env,
|
|
174
|
-
cwd: config.cwd,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const headers: Record<string, string> = {
|
|
179
|
-
...(config.headers ?? {}),
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
183
|
-
requestInit: {
|
|
184
|
-
headers,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
|
-
function toMcpTools(serverName: string, tools: any[]): McpTool[] {
|
|
191
|
-
return tools.map((t) => ({
|
|
192
|
-
name: t.name,
|
|
193
|
-
description: t.description,
|
|
194
|
-
inputSchema: t.inputSchema as Record<string, unknown>,
|
|
195
|
-
outputSchema: t.outputSchema as Record<string, unknown> | undefined,
|
|
196
|
-
annotations: t.annotations as Record<string, unknown> | undefined,
|
|
197
|
-
serverName,
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import { writeFile, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { loadMcpConfig } from "./config-loader.js";
|
|
6
|
-
|
|
7
|
-
describe("loadMcpConfig", () => {
|
|
8
|
-
let dir: string;
|
|
9
|
-
|
|
10
|
-
async function writeConfig(content: string): Promise<string> {
|
|
11
|
-
dir = await mkdtemp(join(tmpdir(), "mcp-config-test-"));
|
|
12
|
-
const path = join(dir, "mcp.json");
|
|
13
|
-
await writeFile(path, content, "utf-8");
|
|
14
|
-
return path;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Clean up temp dir after each test
|
|
18
|
-
afterEach(async () => {
|
|
19
|
-
if (dir) {
|
|
20
|
-
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("returns empty config when file does not exist", async () => {
|
|
25
|
-
const config = await loadMcpConfig("/tmp/does-not-exist-" + Date.now() + ".json");
|
|
26
|
-
expect(config).toEqual({ mcpServers: {} });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("loads a valid stdio server config", async () => {
|
|
30
|
-
const path = await writeConfig(
|
|
31
|
-
JSON.stringify({
|
|
32
|
-
mcpServers: {
|
|
33
|
-
"test-server": {
|
|
34
|
-
type: "stdio",
|
|
35
|
-
command: "node",
|
|
36
|
-
args: ["server.js"],
|
|
37
|
-
env: { API_KEY: "secret" },
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const config = await loadMcpConfig(path);
|
|
44
|
-
expect(config.mcpServers["test-server"]).toEqual({
|
|
45
|
-
type: "stdio",
|
|
46
|
-
command: "node",
|
|
47
|
-
args: ["server.js"],
|
|
48
|
-
env: { API_KEY: "secret" },
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("loads a valid remote server config", async () => {
|
|
53
|
-
const path = await writeConfig(
|
|
54
|
-
JSON.stringify({
|
|
55
|
-
mcpServers: {
|
|
56
|
-
"remote-server": {
|
|
57
|
-
type: "remote",
|
|
58
|
-
url: "https://example.com/mcp",
|
|
59
|
-
headers: { Authorization: "Bearer tok" },
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const config = await loadMcpConfig(path);
|
|
66
|
-
expect(config.mcpServers["remote-server"]).toEqual({
|
|
67
|
-
type: "remote",
|
|
68
|
-
url: "https://example.com/mcp",
|
|
69
|
-
headers: { Authorization: "Bearer tok" },
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("loads mixed server types", async () => {
|
|
74
|
-
const path = await writeConfig(
|
|
75
|
-
JSON.stringify({
|
|
76
|
-
mcpServers: {
|
|
77
|
-
local: { type: "stdio", command: "python", args: ["-m", "mcp_server"] },
|
|
78
|
-
cloud: { type: "remote", url: "https://api.example.com/mcp" },
|
|
79
|
-
},
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const config = await loadMcpConfig(path);
|
|
84
|
-
expect(Object.keys(config.mcpServers)).toEqual(["local", "cloud"]);
|
|
85
|
-
expect(config.mcpServers["local"].type).toBe("stdio");
|
|
86
|
-
expect(config.mcpServers["cloud"].type).toBe("remote");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("throws on invalid JSON", async () => {
|
|
90
|
-
const path = await writeConfig("not json {{{");
|
|
91
|
-
await expect(loadMcpConfig(path)).rejects.toThrow("Invalid JSON");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("throws on schema violation (missing required fields)", async () => {
|
|
95
|
-
const path = await writeConfig(
|
|
96
|
-
JSON.stringify({
|
|
97
|
-
mcpServers: {
|
|
98
|
-
bad: { type: "stdio" }, // missing "command"
|
|
99
|
-
},
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
102
|
-
await expect(loadMcpConfig(path)).rejects.toThrow("Invalid MCP config");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("throws on unknown server type", async () => {
|
|
106
|
-
const path = await writeConfig(
|
|
107
|
-
JSON.stringify({
|
|
108
|
-
mcpServers: {
|
|
109
|
-
bad: { type: "websocket", url: "ws://localhost" },
|
|
110
|
-
},
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
await expect(loadMcpConfig(path)).rejects.toThrow("Invalid MCP config");
|
|
114
|
-
});
|
|
115
|
-
});
|
package/src/mcp/config-loader.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { Value } from "typebox/value";
|
|
5
|
-
import { McpConfig } from "./config.js";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_CONFIG_PATH = join(homedir(), ".config", "mcpi-ext", "mcp.json");
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Load and validate an MCP config file.
|
|
11
|
-
* Returns an empty config if the file doesn't exist.
|
|
12
|
-
* Throws on invalid JSON or schema violations.
|
|
13
|
-
*/
|
|
14
|
-
export async function loadMcpConfig(configPath?: string): Promise<McpConfig> {
|
|
15
|
-
const path = configPath ?? DEFAULT_CONFIG_PATH;
|
|
16
|
-
|
|
17
|
-
let raw: string;
|
|
18
|
-
try {
|
|
19
|
-
raw = await readFile(path, "utf-8");
|
|
20
|
-
} catch (err: unknown) {
|
|
21
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
22
|
-
return { mcpServers: {} };
|
|
23
|
-
}
|
|
24
|
-
throw new Error(`Failed to read MCP config at ${path}: ${(err as Error).message}`, {
|
|
25
|
-
cause: err,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let parsed: unknown;
|
|
30
|
-
try {
|
|
31
|
-
parsed = JSON.parse(raw);
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error(`Invalid JSON in MCP config at ${path}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (!Value.Check(McpConfig, parsed)) {
|
|
37
|
-
const errors = Value.Errors(McpConfig, parsed);
|
|
38
|
-
const details = errors
|
|
39
|
-
.slice(0, 5)
|
|
40
|
-
.map((e) => ` ${e.instancePath || "/"}: ${e.message}`)
|
|
41
|
-
.join("\n");
|
|
42
|
-
throw new Error(`Invalid MCP config at ${path}:\n${details}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return parsed;
|
|
46
|
-
}
|
package/src/mcp/config.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { Type, type Static } from "typebox";
|
|
2
|
-
|
|
3
|
-
/** Configuration for a stdio-based MCP server (spawns a child process). */
|
|
4
|
-
export const StdioServerConfig = Type.Object({
|
|
5
|
-
type: Type.Literal("stdio"),
|
|
6
|
-
command: Type.String({ description: "Executable to run" }),
|
|
7
|
-
args: Type.Optional(Type.Array(Type.String(), { description: "Command-line arguments" })),
|
|
8
|
-
env: Type.Optional(
|
|
9
|
-
Type.Record(Type.String(), Type.String(), {
|
|
10
|
-
description: "Extra environment variables for the child process",
|
|
11
|
-
}),
|
|
12
|
-
),
|
|
13
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the child process" })),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
/** Configuration for a remote MCP server (Streamable HTTP). */
|
|
17
|
-
export const RemoteServerConfig = Type.Object({
|
|
18
|
-
type: Type.Literal("remote"),
|
|
19
|
-
url: Type.String({ description: "HTTP(S) endpoint URL" }),
|
|
20
|
-
headers: Type.Optional(
|
|
21
|
-
Type.Record(Type.String(), Type.String(), {
|
|
22
|
-
description: "Extra HTTP headers (e.g. Authorization)",
|
|
23
|
-
}),
|
|
24
|
-
),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
/** A single MCP server entry — either stdio or remote. */
|
|
28
|
-
export const ServerConfig = Type.Union([StdioServerConfig, RemoteServerConfig]);
|
|
29
|
-
|
|
30
|
-
/** Top-level configuration file schema. */
|
|
31
|
-
export const McpConfig = Type.Object({
|
|
32
|
-
mcpServers: Type.Record(Type.String(), ServerConfig, {
|
|
33
|
-
description: "Named MCP server configurations",
|
|
34
|
-
}),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export type StdioServerConfig = Static<typeof StdioServerConfig>;
|
|
38
|
-
export type RemoteServerConfig = Static<typeof RemoteServerConfig>;
|
|
39
|
-
export type ServerConfig = Static<typeof ServerConfig>;
|
|
40
|
-
export type McpConfig = Static<typeof McpConfig>;
|