@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.
Files changed (208) hide show
  1. package/dist/code-mode/code-mode-integration.test.d.ts +2 -0
  2. package/dist/code-mode/code-mode-integration.test.d.ts.map +1 -0
  3. package/dist/code-mode/code-mode-integration.test.js +116 -0
  4. package/dist/code-mode/code-mode-integration.test.js.map +1 -0
  5. package/dist/code-mode/eligibility.d.ts +10 -0
  6. package/dist/code-mode/eligibility.d.ts.map +1 -0
  7. package/dist/code-mode/eligibility.js +14 -0
  8. package/dist/code-mode/eligibility.js.map +1 -0
  9. package/dist/code-mode/eligibility.test.d.ts +2 -0
  10. package/dist/code-mode/eligibility.test.d.ts.map +1 -0
  11. package/dist/code-mode/eligibility.test.js +53 -0
  12. package/dist/code-mode/eligibility.test.js.map +1 -0
  13. package/dist/code-mode/executor.d.ts +35 -0
  14. package/dist/code-mode/executor.d.ts.map +1 -0
  15. package/dist/code-mode/executor.js +120 -0
  16. package/dist/code-mode/executor.js.map +1 -0
  17. package/dist/code-mode/executor.test.d.ts +2 -0
  18. package/dist/code-mode/executor.test.d.ts.map +1 -0
  19. package/dist/code-mode/executor.test.js +130 -0
  20. package/dist/code-mode/executor.test.js.map +1 -0
  21. package/dist/code-mode/index.d.ts +65 -0
  22. package/dist/code-mode/index.d.ts.map +1 -0
  23. package/dist/code-mode/index.js +175 -0
  24. package/dist/code-mode/index.js.map +1 -0
  25. package/dist/code-mode/tools.d.ts +45 -0
  26. package/dist/code-mode/tools.d.ts.map +1 -0
  27. package/dist/code-mode/tools.js +70 -0
  28. package/dist/code-mode/tools.js.map +1 -0
  29. package/dist/code-mode/type-hints.d.ts +20 -0
  30. package/dist/code-mode/type-hints.d.ts.map +1 -0
  31. package/dist/code-mode/type-hints.js +189 -0
  32. package/dist/code-mode/type-hints.js.map +1 -0
  33. package/dist/code-mode/type-hints.test.d.ts +2 -0
  34. package/dist/code-mode/type-hints.test.d.ts.map +1 -0
  35. package/dist/code-mode/type-hints.test.js +157 -0
  36. package/dist/code-mode/type-hints.test.js.map +1 -0
  37. package/dist/docker-e2e.d.ts +55 -0
  38. package/dist/docker-e2e.d.ts.map +1 -0
  39. package/dist/docker-e2e.js +235 -0
  40. package/dist/docker-e2e.js.map +1 -0
  41. package/dist/docker-e2e.test.d.ts +2 -0
  42. package/dist/docker-e2e.test.d.ts.map +1 -0
  43. package/dist/docker-e2e.test.js +176 -0
  44. package/dist/docker-e2e.test.js.map +1 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +148 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/index.test.d.ts +2 -0
  50. package/dist/index.test.d.ts.map +1 -0
  51. package/dist/index.test.js +8 -0
  52. package/dist/index.test.js.map +1 -0
  53. package/dist/mcp/client-manager.d.ts +46 -0
  54. package/dist/mcp/client-manager.d.ts.map +1 -0
  55. package/dist/mcp/client-manager.js +148 -0
  56. package/dist/mcp/client-manager.js.map +1 -0
  57. package/dist/mcp/client-manager.test.d.ts +2 -0
  58. package/dist/mcp/client-manager.test.d.ts.map +1 -0
  59. package/dist/mcp/client-manager.test.js +204 -0
  60. package/dist/mcp/client-manager.test.js.map +1 -0
  61. package/dist/mcp/config-loader.d.ts +8 -0
  62. package/dist/mcp/config-loader.d.ts.map +1 -0
  63. package/dist/mcp/config-loader.js +43 -0
  64. package/dist/mcp/config-loader.js.map +1 -0
  65. package/dist/mcp/config-loader.test.d.ts +2 -0
  66. package/dist/mcp/config-loader.test.d.ts.map +1 -0
  67. package/dist/mcp/config-loader.test.js +93 -0
  68. package/dist/mcp/config-loader.test.js.map +1 -0
  69. package/dist/mcp/config.d.ts +46 -0
  70. package/dist/mcp/config.d.ts.map +1 -0
  71. package/dist/mcp/config.js +28 -0
  72. package/dist/mcp/config.js.map +1 -0
  73. package/{src/mcp/index.ts → dist/mcp/index.d.ts} +1 -0
  74. package/dist/mcp/index.d.ts.map +1 -0
  75. package/dist/mcp/index.js +4 -0
  76. package/dist/mcp/index.js.map +1 -0
  77. package/dist/skills/discover.d.ts +10 -0
  78. package/dist/skills/discover.d.ts.map +1 -0
  79. package/dist/skills/discover.js +60 -0
  80. package/dist/skills/discover.js.map +1 -0
  81. package/dist/skills/format.d.ts +10 -0
  82. package/dist/skills/format.d.ts.map +1 -0
  83. package/dist/skills/format.js +38 -0
  84. package/dist/skills/format.js.map +1 -0
  85. package/dist/skills/format.test.d.ts +2 -0
  86. package/dist/skills/format.test.d.ts.map +1 -0
  87. package/dist/skills/format.test.js +63 -0
  88. package/dist/skills/format.test.js.map +1 -0
  89. package/{src/skills/index.ts → dist/skills/index.d.ts} +2 -5
  90. package/dist/skills/index.d.ts.map +1 -0
  91. package/dist/skills/index.js +6 -0
  92. package/dist/skills/index.js.map +1 -0
  93. package/dist/skills/load-skill-tool.d.ts +40 -0
  94. package/dist/skills/load-skill-tool.d.ts.map +1 -0
  95. package/dist/skills/load-skill-tool.js +104 -0
  96. package/dist/skills/load-skill-tool.js.map +1 -0
  97. package/dist/skills/mcp-tool-proxy.d.ts +11 -0
  98. package/dist/skills/mcp-tool-proxy.d.ts.map +1 -0
  99. package/dist/skills/mcp-tool-proxy.js +116 -0
  100. package/dist/skills/mcp-tool-proxy.js.map +1 -0
  101. package/dist/skills/skill-integration.test.d.ts +2 -0
  102. package/dist/skills/skill-integration.test.d.ts.map +1 -0
  103. package/dist/skills/skill-integration.test.js +119 -0
  104. package/dist/skills/skill-integration.test.js.map +1 -0
  105. package/dist/skills/skill-registry.d.ts +39 -0
  106. package/dist/skills/skill-registry.d.ts.map +1 -0
  107. package/dist/skills/skill-registry.js +48 -0
  108. package/dist/skills/skill-registry.js.map +1 -0
  109. package/dist/skills/skill-registry.test.d.ts +2 -0
  110. package/dist/skills/skill-registry.test.d.ts.map +1 -0
  111. package/dist/skills/skill-registry.test.js +67 -0
  112. package/dist/skills/skill-registry.test.js.map +1 -0
  113. package/dist/test-servers/weather-server.d.ts +12 -0
  114. package/dist/test-servers/weather-server.d.ts.map +1 -0
  115. package/dist/test-servers/weather-server.js +139 -0
  116. package/dist/test-servers/weather-server.js.map +1 -0
  117. package/dist/test-servers/weather-stdio.d.ts +2 -0
  118. package/dist/test-servers/weather-stdio.d.ts.map +1 -0
  119. package/{src/test-servers/weather-stdio.ts → dist/test-servers/weather-stdio.js} +1 -1
  120. package/dist/test-servers/weather-stdio.js.map +1 -0
  121. package/dist/tool-cli/cli.d.ts +3 -0
  122. package/dist/tool-cli/cli.d.ts.map +1 -0
  123. package/dist/tool-cli/cli.js +179 -0
  124. package/dist/tool-cli/cli.js.map +1 -0
  125. package/dist/tool-cli/constants.d.ts +7 -0
  126. package/dist/tool-cli/constants.d.ts.map +1 -0
  127. package/dist/tool-cli/constants.js +15 -0
  128. package/dist/tool-cli/constants.js.map +1 -0
  129. package/dist/tool-cli/format.d.ts +8 -0
  130. package/dist/tool-cli/format.d.ts.map +1 -0
  131. package/{src/tool-cli/format.ts → dist/tool-cli/format.js} +5 -4
  132. package/dist/tool-cli/format.js.map +1 -0
  133. package/dist/tool-cli/format.test.d.ts +2 -0
  134. package/dist/tool-cli/format.test.d.ts.map +1 -0
  135. package/dist/tool-cli/format.test.js +30 -0
  136. package/dist/tool-cli/format.test.js.map +1 -0
  137. package/{src/tool-cli/index.ts → dist/tool-cli/index.d.ts} +1 -0
  138. package/dist/tool-cli/index.d.ts.map +1 -0
  139. package/dist/tool-cli/index.js +3 -0
  140. package/dist/tool-cli/index.js.map +1 -0
  141. package/dist/tool-cli/rpc-client.d.ts +6 -0
  142. package/dist/tool-cli/rpc-client.d.ts.map +1 -0
  143. package/dist/tool-cli/rpc-client.js +25 -0
  144. package/dist/tool-cli/rpc-client.js.map +1 -0
  145. package/dist/tool-cli/rpc-server.d.ts +48 -0
  146. package/dist/tool-cli/rpc-server.d.ts.map +1 -0
  147. package/dist/tool-cli/rpc-server.js +220 -0
  148. package/dist/tool-cli/rpc-server.js.map +1 -0
  149. package/dist/tool-cli/rpc-server.test.d.ts +2 -0
  150. package/dist/tool-cli/rpc-server.test.d.ts.map +1 -0
  151. package/dist/tool-cli/rpc-server.test.js +154 -0
  152. package/dist/tool-cli/rpc-server.test.js.map +1 -0
  153. package/package.json +5 -1
  154. package/.dockerignore +0 -6
  155. package/.github/dependabot.yml +0 -25
  156. package/.github/workflows/ci.yml +0 -50
  157. package/.hadolint.yaml +0 -6
  158. package/.prettierignore +0 -4
  159. package/.prettierrc +0 -7
  160. package/AGENTS.md +0 -118
  161. package/DECISIONS.md +0 -99
  162. package/Dockerfile +0 -39
  163. package/aube-lock.yaml +0 -4146
  164. package/docs/code-mode.md +0 -38
  165. package/docs/skills.md +0 -57
  166. package/docs/tool-cli.md +0 -54
  167. package/eslint.config.js +0 -27
  168. package/images/banner.webp +0 -0
  169. package/images/code-c-maude.webp +0 -0
  170. package/images/code-mode-histogram.png +0 -0
  171. package/images/nuclear-mcp-football.webp +0 -0
  172. package/images/skills-enabling-mcp-tools.png +0 -0
  173. package/images/the-skill-dealer.webp +0 -0
  174. package/images/tool-cli-grep.png +0 -0
  175. package/mise.toml +0 -53
  176. package/scripts/docker-entrypoint.sh +0 -29
  177. package/scripts/test-echo-server.mjs +0 -28
  178. package/scripts/test-mcp-integration.mjs +0 -59
  179. package/scripts/test-tool-cli-smoke.mjs +0 -93
  180. package/src/code-mode/code-mode-integration.test.ts +0 -135
  181. package/src/code-mode/eligibility.test.ts +0 -60
  182. package/src/code-mode/eligibility.ts +0 -16
  183. package/src/code-mode/executor.test.ts +0 -168
  184. package/src/code-mode/executor.ts +0 -163
  185. package/src/code-mode/index.ts +0 -208
  186. package/src/code-mode/tools.ts +0 -110
  187. package/src/code-mode/type-hints.test.ts +0 -187
  188. package/src/code-mode/type-hints.ts +0 -249
  189. package/src/docker-e2e.test.ts +0 -195
  190. package/src/docker-e2e.ts +0 -277
  191. package/src/index.test.ts +0 -8
  192. package/src/index.ts +0 -179
  193. package/src/mcp/client-manager.test.ts +0 -250
  194. package/src/mcp/client-manager.ts +0 -199
  195. package/src/mcp/config-loader.test.ts +0 -115
  196. package/src/mcp/config-loader.ts +0 -46
  197. package/src/mcp/config.ts +0 -40
  198. package/src/skills/discover.ts +0 -81
  199. package/src/skills/format.test.ts +0 -70
  200. package/src/skills/format.ts +0 -42
  201. package/src/skills/load-skill-tool.ts +0 -136
  202. package/src/skills/mcp-tool-proxy.ts +0 -143
  203. package/src/skills/skill-integration.test.ts +0 -139
  204. package/src/skills/skill-registry.test.ts +0 -83
  205. package/src/skills/skill-registry.ts +0 -69
  206. package/src/test-servers/weather-server.ts +0 -171
  207. package/src/tool-cli/format.test.ts +0 -33
  208. 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
- });
@@ -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>;