pi-soly 1.9.3 → 1.11.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.
Files changed (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,218 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ lazyConnect: vi.fn(),
5
+ getFailureAgeSeconds: vi.fn(),
6
+ authenticate: vi.fn(),
7
+ supportsOAuth: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("../init.ts", () => ({
11
+ lazyConnect: mocks.lazyConnect,
12
+ getFailureAgeSeconds: mocks.getFailureAgeSeconds,
13
+ }));
14
+
15
+ vi.mock("../mcp-auth-flow.ts", () => ({
16
+ authenticate: mocks.authenticate,
17
+ supportsOAuth: mocks.supportsOAuth,
18
+ }));
19
+
20
+ describe("direct tools auto auth", () => {
21
+ beforeEach(() => {
22
+ vi.resetModules();
23
+ mocks.lazyConnect.mockReset();
24
+ mocks.getFailureAgeSeconds.mockReset().mockReturnValue(null);
25
+ mocks.authenticate.mockReset().mockResolvedValue("authenticated");
26
+ mocks.supportsOAuth.mockReset().mockReturnValue(true);
27
+ });
28
+
29
+ it("auto-authenticates and retries direct tool execution once", async () => {
30
+ const { createDirectToolExecutor } = await import("../direct-tools.ts");
31
+
32
+ let connection: any = { status: "needs-auth" };
33
+ const connected = {
34
+ status: "connected",
35
+ client: {
36
+ callTool: vi.fn(async () => ({
37
+ isError: false,
38
+ content: [{ type: "text", text: "ok" }],
39
+ })),
40
+ },
41
+ };
42
+
43
+ mocks.lazyConnect
44
+ .mockImplementationOnce(async () => false)
45
+ .mockImplementationOnce(async () => {
46
+ connection = connected;
47
+ return true;
48
+ });
49
+
50
+ const state = {
51
+ config: {
52
+ settings: { autoAuth: true },
53
+ mcpServers: {
54
+ demo: { url: "https://api.example.com/mcp", auth: "oauth" },
55
+ },
56
+ },
57
+ manager: {
58
+ close: vi.fn(async () => {
59
+ connection = undefined;
60
+ }),
61
+ getConnection: vi.fn(() => connection),
62
+ touch: vi.fn(),
63
+ incrementInFlight: vi.fn(),
64
+ decrementInFlight: vi.fn(),
65
+ },
66
+ failureTracker: new Map(),
67
+ ui: { setStatus: vi.fn() },
68
+ completedUiSessions: [],
69
+ } as any;
70
+
71
+ const executor = createDirectToolExecutor(
72
+ () => state,
73
+ () => null,
74
+ {
75
+ serverName: "demo",
76
+ originalName: "search",
77
+ prefixedName: "demo_search",
78
+ description: "Search",
79
+ },
80
+ );
81
+
82
+ const result = await executor("id", { q: "hello" }, undefined as any, () => {}, undefined as any);
83
+
84
+ expect(mocks.authenticate).toHaveBeenCalledWith(
85
+ "demo",
86
+ "https://api.example.com/mcp",
87
+ state.config.mcpServers.demo,
88
+ );
89
+ expect(state.manager.close).toHaveBeenCalledWith("demo");
90
+ expect(result.content[0].text).toContain("ok");
91
+ });
92
+
93
+ it("fails fast in non-ui context for browser-based OAuth", async () => {
94
+ const { createDirectToolExecutor } = await import("../direct-tools.ts");
95
+
96
+ const state = {
97
+ config: {
98
+ settings: { autoAuth: true },
99
+ mcpServers: {
100
+ demo: { url: "https://api.example.com/mcp", auth: "oauth" },
101
+ },
102
+ },
103
+ manager: {
104
+ close: vi.fn(async () => {}),
105
+ getConnection: vi.fn(() => ({ status: "needs-auth" })),
106
+ touch: vi.fn(),
107
+ incrementInFlight: vi.fn(),
108
+ decrementInFlight: vi.fn(),
109
+ },
110
+ failureTracker: new Map(),
111
+ ui: undefined,
112
+ completedUiSessions: [],
113
+ } as any;
114
+
115
+ mocks.lazyConnect.mockResolvedValue(false);
116
+
117
+ const executor = createDirectToolExecutor(
118
+ () => state,
119
+ () => null,
120
+ {
121
+ serverName: "demo",
122
+ originalName: "search",
123
+ prefixedName: "demo_search",
124
+ description: "Search",
125
+ },
126
+ );
127
+
128
+ const result = await executor("id", {}, undefined as any, () => {}, undefined as any);
129
+
130
+ expect(mocks.authenticate).not.toHaveBeenCalled();
131
+ expect(result.content[0].text).toContain("auth-start");
132
+ expect(result.content[0].text).toContain("/mcp-auth demo");
133
+ });
134
+
135
+ it("runs URL elicitations returned by a URL-required tool error", async () => {
136
+ const { UrlElicitationRequiredError } = await import("@modelcontextprotocol/sdk/types.js");
137
+ const { createDirectToolExecutor } = await import("../direct-tools.ts");
138
+ const error = new UrlElicitationRequiredError([{
139
+ mode: "url",
140
+ message: "Connect your account",
141
+ elicitationId: "connect-1",
142
+ url: "https://example.com/connect",
143
+ }]);
144
+ const connection = {
145
+ status: "connected",
146
+ client: { callTool: vi.fn().mockRejectedValue(error) },
147
+ };
148
+ const state = {
149
+ config: { settings: {}, mcpServers: { demo: { command: "demo" } } },
150
+ manager: {
151
+ getConnection: vi.fn(() => connection),
152
+ handleUrlElicitationRequired: vi.fn().mockResolvedValue("accept"),
153
+ touch: vi.fn(),
154
+ incrementInFlight: vi.fn(),
155
+ decrementInFlight: vi.fn(),
156
+ },
157
+ failureTracker: new Map(),
158
+ completedUiSessions: [],
159
+ } as any;
160
+ mocks.lazyConnect.mockResolvedValue(true);
161
+
162
+ const executor = createDirectToolExecutor(() => state, () => null, {
163
+ serverName: "demo",
164
+ originalName: "search",
165
+ prefixedName: "demo_search",
166
+ description: "Search",
167
+ });
168
+ const result = await executor("id", {}, undefined, undefined, undefined as any);
169
+
170
+ expect(state.manager.handleUrlElicitationRequired).toHaveBeenCalledWith("demo", error);
171
+ expect(result.details).toMatchObject({ error: "url_elicitation_required", action: "accept" });
172
+ expect(result.content[0].text).toContain("retry the tool");
173
+ });
174
+
175
+ it("uses custom authRequiredMessage in non-ui direct tool auth failures", async () => {
176
+ const { createDirectToolExecutor } = await import("../direct-tools.ts");
177
+
178
+ const state = {
179
+ config: {
180
+ settings: {
181
+ autoAuth: true,
182
+ authRequiredMessage: "Reconnect ${server} from the host app.",
183
+ },
184
+ mcpServers: {
185
+ demo: { url: "https://api.example.com/mcp", auth: "oauth" },
186
+ },
187
+ },
188
+ manager: {
189
+ close: vi.fn(async () => {}),
190
+ getConnection: vi.fn(() => ({ status: "needs-auth" })),
191
+ touch: vi.fn(),
192
+ incrementInFlight: vi.fn(),
193
+ decrementInFlight: vi.fn(),
194
+ },
195
+ failureTracker: new Map(),
196
+ ui: undefined,
197
+ completedUiSessions: [],
198
+ } as any;
199
+
200
+ mocks.lazyConnect.mockResolvedValue(false);
201
+
202
+ const executor = createDirectToolExecutor(
203
+ () => state,
204
+ () => null,
205
+ {
206
+ serverName: "demo",
207
+ originalName: "search",
208
+ prefixedName: "demo_search",
209
+ description: "Search",
210
+ },
211
+ );
212
+
213
+ const result = await executor("id", {}, undefined as any, () => {}, undefined as any);
214
+
215
+ expect(mocks.authenticate).not.toHaveBeenCalled();
216
+ expect(result.content[0].text).toBe("Reconnect demo from the host app.");
217
+ });
218
+ });
@@ -0,0 +1,299 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { buildProxyDescription, resolveDirectTools } from "../direct-tools.ts";
5
+ import { computeServerHash, isServerCacheValid, type MetadataCache } from "../metadata-cache.ts";
6
+ import { buildToolMetadata } from "../tool-metadata.ts";
7
+ import type { McpConfig } from "../types.ts";
8
+ import { reconstructToolMetadata } from "../metadata-cache.ts";
9
+
10
+ const originalHashEnv = {
11
+ MCP_HASH_CWD: process.env.MCP_HASH_CWD,
12
+ MCP_HASH_ENV: process.env.MCP_HASH_ENV,
13
+ MCP_HASH_HEADER: process.env.MCP_HASH_HEADER,
14
+ MCP_HASH_TOKEN: process.env.MCP_HASH_TOKEN,
15
+ };
16
+
17
+ afterEach(() => {
18
+ for (const [key, value] of Object.entries(originalHashEnv)) {
19
+ if (value === undefined) {
20
+ delete process.env[key];
21
+ } else {
22
+ process.env[key] = value;
23
+ }
24
+ }
25
+ });
26
+
27
+ describe("buildProxyDescription", () => {
28
+ it("documents the ui-messages action", () => {
29
+ const config: McpConfig = {
30
+ mcpServers: {
31
+ demo: {
32
+ command: "npx",
33
+ args: ["-y", "demo-server"],
34
+ },
35
+ },
36
+ };
37
+
38
+ const cache: MetadataCache = {
39
+ version: 1,
40
+ servers: {
41
+ demo: {
42
+ configHash: "hash",
43
+ cachedAt: Date.now(),
44
+ tools: [
45
+ {
46
+ name: "launch_app",
47
+ description: "Launch the demo app",
48
+ inputSchema: { type: "object", properties: {} },
49
+ },
50
+ ],
51
+ resources: [],
52
+ },
53
+ },
54
+ };
55
+
56
+ const description = buildProxyDescription(config, cache, []);
57
+
58
+ expect(description).toContain('mcp({ action: "ui-messages" })');
59
+ expect(description).toContain("Retrieve accumulated messages from completed UI sessions");
60
+ expect(description).toContain("Search MCP tools by name/description");
61
+ expect(description).toContain("Non-MCP Pi tools should be called directly, not through mcp.");
62
+ expect(description).not.toContain("MCP + pi");
63
+ });
64
+
65
+ it("excludes configured tools from proxy summaries", () => {
66
+ const config: McpConfig = {
67
+ settings: { toolPrefix: "server" },
68
+ mcpServers: {
69
+ figma: {
70
+ command: "npx",
71
+ args: ["-y", "figma"],
72
+ excludeTools: ["get_figjam", "figma_get_screenshot"],
73
+ },
74
+ },
75
+ };
76
+
77
+ const cache: MetadataCache = {
78
+ version: 1,
79
+ servers: {
80
+ figma: {
81
+ configHash: computeServerHash(config.mcpServers.figma),
82
+ cachedAt: Date.now(),
83
+ tools: [
84
+ { name: "get_screenshot", description: "Take screenshot" },
85
+ { name: "get_nodes", description: "Get nodes" },
86
+ ],
87
+ resources: [
88
+ { name: "figjam", uri: "ui://figjam", description: "FigJam" },
89
+ ],
90
+ },
91
+ },
92
+ };
93
+
94
+ const description = buildProxyDescription(config, cache, []);
95
+
96
+ expect(description).toContain("Servers: figma (1 tools)");
97
+ expect(description).not.toContain("figma (3 tools)");
98
+ });
99
+ });
100
+
101
+ describe("metadata cache hashing", () => {
102
+ it("hashes interpolated cwd", () => {
103
+ process.env.MCP_HASH_CWD = "/tmp/mcp-one";
104
+ const first = computeServerHash({ command: "node", cwd: "${MCP_HASH_CWD}/server" });
105
+
106
+ process.env.MCP_HASH_CWD = "/tmp/mcp-two";
107
+ const second = computeServerHash({ command: "node", cwd: "${MCP_HASH_CWD}/server" });
108
+
109
+ expect(first).not.toBe(second);
110
+ expect(computeServerHash({ command: "node", cwd: "${MCP_HASH_CWD}/server" })).toBe(
111
+ computeServerHash({ command: "node", cwd: "/tmp/mcp-two/server" }),
112
+ );
113
+ });
114
+
115
+ it("hashes interpolated env values", () => {
116
+ process.env.MCP_HASH_ENV = "/tmp/data-one";
117
+ const first = computeServerHash({ command: "node", env: { DATA_DIR: "${MCP_HASH_ENV}" } });
118
+
119
+ process.env.MCP_HASH_ENV = "/tmp/data-two";
120
+ const second = computeServerHash({ command: "node", env: { DATA_DIR: "${MCP_HASH_ENV}" } });
121
+
122
+ expect(first).not.toBe(second);
123
+ expect(computeServerHash({ command: "node", env: { DATA_DIR: "${MCP_HASH_ENV}" } })).toBe(
124
+ computeServerHash({ command: "node", env: { DATA_DIR: "/tmp/data-two" } }),
125
+ );
126
+ });
127
+
128
+ it("hashes interpolated header values", () => {
129
+ process.env.MCP_HASH_HEADER = "header-one";
130
+ const first = computeServerHash({ url: "https://example.test/mcp", headers: { "x-root": "$env:MCP_HASH_HEADER" } });
131
+
132
+ process.env.MCP_HASH_HEADER = "header-two";
133
+ const second = computeServerHash({ url: "https://example.test/mcp", headers: { "x-root": "$env:MCP_HASH_HEADER" } });
134
+
135
+ expect(first).not.toBe(second);
136
+ expect(computeServerHash({ url: "https://example.test/mcp", headers: { "x-root": "$env:MCP_HASH_HEADER" } })).toBe(
137
+ computeServerHash({ url: "https://example.test/mcp", headers: { "x-root": "header-two" } }),
138
+ );
139
+ });
140
+
141
+ it("hashes tilde cwd as the home directory", () => {
142
+ expect(computeServerHash({ command: "node", cwd: "~/server" })).toBe(
143
+ computeServerHash({ command: "node", cwd: join(homedir(), "server") }),
144
+ );
145
+ });
146
+
147
+ it("hashes the effective bearerTokenEnv value", () => {
148
+ process.env.MCP_HASH_TOKEN = "token-one";
149
+ const first = computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerTokenEnv: "MCP_HASH_TOKEN" });
150
+
151
+ process.env.MCP_HASH_TOKEN = "token-two";
152
+ const second = computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerTokenEnv: "MCP_HASH_TOKEN" });
153
+
154
+ expect(first).not.toBe(second);
155
+ expect(computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerTokenEnv: "MCP_HASH_TOKEN" })).toBe(
156
+ computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerToken: "token-two", bearerTokenEnv: "MCP_HASH_TOKEN" }),
157
+ );
158
+ });
159
+
160
+ it("hashes interpolated bearerToken values", () => {
161
+ process.env.MCP_HASH_TOKEN = "token-one";
162
+ const first = computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerToken: "${MCP_HASH_TOKEN}" });
163
+
164
+ process.env.MCP_HASH_TOKEN = "token-two";
165
+ const second = computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerToken: "${MCP_HASH_TOKEN}" });
166
+
167
+ expect(first).not.toBe(second);
168
+ expect(computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerToken: "$env:MCP_HASH_TOKEN" })).toBe(
169
+ computeServerHash({ url: "https://example.test/mcp", auth: "bearer", bearerToken: "token-two" }),
170
+ );
171
+ });
172
+
173
+ it("invalidates cached metadata when an interpolated bearerToken env value changes", () => {
174
+ const definition = { url: "https://example.test/mcp", auth: "bearer" as const, bearerToken: "${MCP_HASH_TOKEN}" };
175
+ process.env.MCP_HASH_TOKEN = "token-one";
176
+ const entry = {
177
+ configHash: computeServerHash(definition),
178
+ cachedAt: Date.now(),
179
+ tools: [],
180
+ resources: [],
181
+ };
182
+
183
+ expect(isServerCacheValid(entry, definition)).toBe(true);
184
+
185
+ process.env.MCP_HASH_TOKEN = "token-two";
186
+
187
+ expect(isServerCacheValid(entry, definition)).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe("excludeTools filtering", () => {
192
+ it("filters excluded tools from live and cached metadata", () => {
193
+ const definition = {
194
+ command: "npx",
195
+ args: ["-y", "figma"],
196
+ excludeTools: ["figma_get_screenshot", "get_figjam"],
197
+ };
198
+
199
+ const { metadata } = buildToolMetadata(
200
+ [
201
+ { name: "get_screenshot", description: "Screenshot" },
202
+ { name: "get_nodes", description: "Nodes" },
203
+ ] as any,
204
+ [
205
+ { name: "figjam", uri: "ui://figjam", description: "FigJam" },
206
+ ] as any,
207
+ definition,
208
+ "figma",
209
+ "server",
210
+ );
211
+
212
+ expect(metadata.map((tool) => tool.name)).toEqual(["figma_get_nodes"]);
213
+
214
+ const reconstructed = reconstructToolMetadata(
215
+ "figma",
216
+ {
217
+ configHash: computeServerHash(definition),
218
+ cachedAt: Date.now(),
219
+ tools: [
220
+ { name: "get_screenshot", description: "Screenshot" },
221
+ { name: "get_nodes", description: "Nodes" },
222
+ ],
223
+ resources: [{ name: "figjam", uri: "ui://figjam", description: "FigJam" }],
224
+ },
225
+ "server",
226
+ definition,
227
+ );
228
+
229
+ expect(reconstructed.map((tool) => tool.name)).toEqual(["figma_get_nodes"]);
230
+ });
231
+
232
+ it("filters excluded tools during direct tool registration from cache", () => {
233
+ const config: McpConfig = {
234
+ settings: { toolPrefix: "server" },
235
+ mcpServers: {
236
+ figma: {
237
+ command: "npx",
238
+ args: ["-y", "figma"],
239
+ directTools: true,
240
+ excludeTools: ["figma_get_screenshot", "get_figjam"],
241
+ },
242
+ },
243
+ };
244
+
245
+ const cache: MetadataCache = {
246
+ version: 1,
247
+ servers: {
248
+ figma: {
249
+ configHash: computeServerHash(config.mcpServers.figma),
250
+ cachedAt: Date.now(),
251
+ tools: [
252
+ { name: "get_screenshot", description: "Screenshot" },
253
+ { name: "get_nodes", description: "Nodes" },
254
+ ],
255
+ resources: [
256
+ { name: "figjam", uri: "ui://figjam", description: "FigJam" },
257
+ ],
258
+ },
259
+ },
260
+ };
261
+
262
+ const specs = resolveDirectTools(config, cache, "server");
263
+
264
+ expect(specs.map((spec) => spec.prefixedName)).toEqual(["figma_get_nodes"]);
265
+ });
266
+
267
+ it("matches prefixed exclusions even when toolPrefix is none", () => {
268
+ const config: McpConfig = {
269
+ settings: { toolPrefix: "none" },
270
+ mcpServers: {
271
+ figma: {
272
+ command: "npx",
273
+ args: ["-y", "figma"],
274
+ directTools: true,
275
+ excludeTools: ["figma_get_screenshot"],
276
+ },
277
+ },
278
+ };
279
+
280
+ const cache: MetadataCache = {
281
+ version: 1,
282
+ servers: {
283
+ figma: {
284
+ configHash: computeServerHash(config.mcpServers.figma),
285
+ cachedAt: Date.now(),
286
+ tools: [
287
+ { name: "get_screenshot", description: "Screenshot" },
288
+ { name: "get_nodes", description: "Nodes" },
289
+ ],
290
+ resources: [],
291
+ },
292
+ },
293
+ };
294
+
295
+ const specs = resolveDirectTools(config, cache, "none");
296
+
297
+ expect(specs.map((spec) => spec.prefixedName)).toEqual(["get_nodes"]);
298
+ });
299
+ });