pi-mcp-adapter 2.3.4 → 2.4.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/CHANGELOG.md +20 -0
- package/README.md +21 -0
- package/direct-tools.ts +97 -6
- package/index.ts +92 -79
- package/init.ts +3 -12
- package/mcp-auth-flow.ts +61 -40
- package/mcp-callback-server.ts +9 -4
- package/mcp-panel.ts +40 -13
- package/metadata-cache.ts +12 -3
- package/package.json +1 -1
- package/proxy-modes.ts +169 -25
- package/tool-metadata.ts +9 -1
- package/types.ts +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.0] - 2026-04-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `settings.disableProxyTool` to hide the `mcp` proxy tool once configured direct tools are fully available from cache. Thanks @tanavamsikrishna for PR #41.
|
|
14
|
+
- Per-server `excludeTools` to hide specific MCP tools/resources by original or prefixed name across direct tools, proxy discovery, and the `/mcp` panel. Thanks @ahmadaccino for issue #36.
|
|
15
|
+
- `settings.autoAuth` to optionally trigger OAuth automatically from proxy/direct tool usage, then rerun the original blocked connect/tool operation once after authentication succeeds. Thanks @unimonkiez for issue #34.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Regenerated `package-lock.json` so the root lockfile metadata matches `package.json` again, including the declared `open`, `@types/bun`, `@types/open`, and `tsx` entries.
|
|
19
|
+
- Kept the `mcp` proxy tool available as a first-session fallback when configured direct tools are still missing cache metadata, avoiding no-tool startup gaps.
|
|
20
|
+
|
|
21
|
+
## [2.3.5] - 2026-04-13
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Session lifecycle now always tears down OAuth callback state on restart and shutdown, preventing callback-server leaks across session transitions.
|
|
25
|
+
- OAuth callback server now calls `unref()` after successful bind so it no longer keeps sub-agent processes alive by itself.
|
|
26
|
+
- Strict OAuth port mode now rebinds to the configured callback port when safe, while refusing to switch ports when authorizations are still pending.
|
|
27
|
+
- Added focused lifecycle/callback-server regression coverage for teardown, `unref()`, strict rebinding, and pending-auth guardrails.
|
|
28
|
+
- Thanks @blai for the investigation and PR #43 that surfaced the sub-agent hang/root lifecycle issues.
|
|
29
|
+
|
|
10
30
|
## [2.3.4] - 2026-04-12
|
|
11
31
|
|
|
12
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ Two calls instead of 26 tools cluttering the context.
|
|
|
91
91
|
| `idleTimeout` | Minutes before idle disconnect (overrides global) |
|
|
92
92
|
| `exposeResources` | Expose MCP resources as tools (default: true) |
|
|
93
93
|
| `directTools` | `true`, `string[]`, or `false` — register tools individually instead of through proxy |
|
|
94
|
+
| `excludeTools` | `string[]` of tool names to hide (matches original names like `get_screenshot` and prefixed names like `figma_get_screenshot`) |
|
|
94
95
|
| `debug` | Show server stderr (default: false) |
|
|
95
96
|
|
|
96
97
|
### Lifecycle Modes
|
|
@@ -116,6 +117,8 @@ Two calls instead of 26 tools cluttering the context.
|
|
|
116
117
|
| `toolPrefix` | `"server"` (default), `"short"` (strips `-mcp` suffix), or `"none"` |
|
|
117
118
|
| `idleTimeout` | Global idle timeout in minutes (default: 10, 0 to disable) |
|
|
118
119
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
120
|
+
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
121
|
+
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
119
122
|
|
|
120
123
|
Per-server `idleTimeout` overrides the global setting.
|
|
121
124
|
|
|
@@ -169,6 +172,22 @@ To set a global default for all servers:
|
|
|
169
172
|
|
|
170
173
|
Per-server `directTools` overrides the global setting. The example above registers direct tools for every server except `huge-server`.
|
|
171
174
|
|
|
175
|
+
To exclude specific tools while still using `directTools: true`, add `excludeTools` on the server:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"mcpServers": {
|
|
180
|
+
"figma": {
|
|
181
|
+
"url": "http://localhost:3845/mcp",
|
|
182
|
+
"directTools": true,
|
|
183
|
+
"excludeTools": ["get_figjam", "figma_get_code_connect_map"]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`excludeTools` filters direct tools, proxy search/list/describe, and the `/mcp` panel view.
|
|
190
|
+
|
|
172
191
|
Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`.
|
|
173
192
|
|
|
174
193
|
Direct tools register from the metadata cache (`~/.pi/agent/mcp-cache.json`), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only and the cache populates in the background. Restart Pi and they'll be available. To force it: `/mcp reconnect <server>` then restart.
|
|
@@ -279,6 +298,8 @@ Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_li
|
|
|
279
298
|
| `/mcp reconnect <server>` | Connect or reconnect a single server |
|
|
280
299
|
| `/mcp-auth <server>` | OAuth setup |
|
|
281
300
|
|
|
301
|
+
If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct tool calls will automatically run OAuth when needed and retry once. In non-interactive sessions, browser-based OAuth still requires running `/mcp-auth <server>` manually.
|
|
302
|
+
|
|
282
303
|
## How It Works
|
|
283
304
|
|
|
284
305
|
- One `mcp` tool in context (~200 tokens) instead of hundreds
|
package/direct-tools.ts
CHANGED
|
@@ -7,11 +7,50 @@ import { isServerCacheValid } from "./metadata-cache.js";
|
|
|
7
7
|
import { formatSchema } from "./tool-metadata.js";
|
|
8
8
|
import { transformMcpContent } from "./tool-registrar.js";
|
|
9
9
|
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
|
|
10
|
-
import { formatToolName } from "./types.js";
|
|
10
|
+
import { formatToolName, isToolExcluded } from "./types.js";
|
|
11
11
|
import { resourceNameToToolName } from "./resource-tools.js";
|
|
12
|
+
import { authenticate, supportsOAuth } from "./mcp-auth-flow.js";
|
|
12
13
|
|
|
13
14
|
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
14
15
|
|
|
16
|
+
type DirectAutoAuthResult =
|
|
17
|
+
| { status: "skipped" }
|
|
18
|
+
| { status: "success" }
|
|
19
|
+
| { status: "failed"; message: string };
|
|
20
|
+
|
|
21
|
+
async function attemptDirectAutoAuth(
|
|
22
|
+
state: McpExtensionState,
|
|
23
|
+
serverName: string,
|
|
24
|
+
): Promise<DirectAutoAuthResult> {
|
|
25
|
+
if (state.config.settings?.autoAuth !== true) {
|
|
26
|
+
return { status: "skipped" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const definition = state.config.mcpServers[serverName];
|
|
30
|
+
if (!definition || !supportsOAuth(definition) || !definition.url) {
|
|
31
|
+
return { status: "skipped" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const grantType = definition.oauth?.grantType ?? "authorization_code";
|
|
35
|
+
if (!state.ui && grantType !== "client_credentials") {
|
|
36
|
+
return {
|
|
37
|
+
status: "failed",
|
|
38
|
+
message: `MCP server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} in an interactive session.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await authenticate(serverName, definition.url, definition);
|
|
44
|
+
return { status: "success" };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
return {
|
|
48
|
+
status: "failed",
|
|
49
|
+
message: `OAuth authentication failed for "${serverName}": ${message}. Run /mcp-auth ${serverName} first.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
export function resolveDirectTools(
|
|
16
55
|
config: McpConfig,
|
|
17
56
|
cache: MetadataCache | null,
|
|
@@ -68,6 +107,7 @@ export function resolveDirectTools(
|
|
|
68
107
|
|
|
69
108
|
for (const tool of serverCache.tools ?? []) {
|
|
70
109
|
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
110
|
+
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
|
|
71
111
|
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
72
112
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
73
113
|
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
@@ -93,6 +133,7 @@ export function resolveDirectTools(
|
|
|
93
133
|
for (const resource of serverCache.resources ?? []) {
|
|
94
134
|
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
95
135
|
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
136
|
+
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
|
|
96
137
|
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
97
138
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
98
139
|
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
@@ -117,11 +158,35 @@ export function resolveDirectTools(
|
|
|
117
158
|
return specs;
|
|
118
159
|
}
|
|
119
160
|
|
|
161
|
+
export function getMissingConfiguredDirectToolServers(
|
|
162
|
+
config: McpConfig,
|
|
163
|
+
cache: MetadataCache | null,
|
|
164
|
+
): string[] {
|
|
165
|
+
const missing: string[] = [];
|
|
166
|
+
const globalDirect = config.settings?.directTools;
|
|
167
|
+
|
|
168
|
+
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
169
|
+
const hasDirectTools = definition.directTools !== undefined
|
|
170
|
+
? !!definition.directTools
|
|
171
|
+
: !!globalDirect;
|
|
172
|
+
|
|
173
|
+
if (!hasDirectTools) continue;
|
|
174
|
+
|
|
175
|
+
const serverCache = cache?.servers?.[serverName];
|
|
176
|
+
if (!serverCache || !isServerCacheValid(serverCache, definition)) {
|
|
177
|
+
missing.push(serverName);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return missing;
|
|
182
|
+
}
|
|
183
|
+
|
|
120
184
|
export function buildProxyDescription(
|
|
121
185
|
config: McpConfig,
|
|
122
186
|
cache: MetadataCache | null,
|
|
123
187
|
directSpecs: DirectToolSpec[],
|
|
124
188
|
): string {
|
|
189
|
+
const prefix = config.settings?.toolPrefix ?? "server";
|
|
125
190
|
let desc = `MCP gateway - connect to MCP servers and call their tools.\n`;
|
|
126
191
|
|
|
127
192
|
const directByServer = new Map<string, number>();
|
|
@@ -139,8 +204,15 @@ export function buildProxyDescription(
|
|
|
139
204
|
for (const serverName of Object.keys(config.mcpServers)) {
|
|
140
205
|
const entry = cache?.servers?.[serverName];
|
|
141
206
|
const definition = config.mcpServers[serverName];
|
|
142
|
-
const toolCount = entry?.tools
|
|
143
|
-
|
|
207
|
+
const toolCount = (entry?.tools ?? []).filter(
|
|
208
|
+
(tool) => !isToolExcluded(tool.name, serverName, prefix, definition.excludeTools),
|
|
209
|
+
).length;
|
|
210
|
+
const resourceCount = definition?.exposeResources !== false
|
|
211
|
+
? (entry?.resources ?? []).filter((resource) => {
|
|
212
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
213
|
+
return !isToolExcluded(baseName, serverName, prefix, definition.excludeTools);
|
|
214
|
+
}).length
|
|
215
|
+
: 0;
|
|
144
216
|
const totalItems = toolCount + resourceCount;
|
|
145
217
|
if (totalItems === 0) continue;
|
|
146
218
|
const directCount = directByServer.get(serverName) ?? 0;
|
|
@@ -196,13 +268,32 @@ export function createDirectToolExecutor(
|
|
|
196
268
|
};
|
|
197
269
|
}
|
|
198
270
|
|
|
199
|
-
|
|
271
|
+
let connected = await lazyConnect(state, spec.serverName);
|
|
272
|
+
let autoAuthAttempted = false;
|
|
273
|
+
|
|
274
|
+
if (!connected && state.manager.getConnection(spec.serverName)?.status === "needs-auth") {
|
|
275
|
+
autoAuthAttempted = true;
|
|
276
|
+
const autoAuth = await attemptDirectAutoAuth(state, spec.serverName);
|
|
277
|
+
if (autoAuth.status === "failed") {
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
280
|
+
details: { error: "auth_required", server: spec.serverName, message: autoAuth.message },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (autoAuth.status === "success") {
|
|
284
|
+
await state.manager.close(spec.serverName);
|
|
285
|
+
state.failureTracker.delete(spec.serverName);
|
|
286
|
+
connected = await lazyConnect(state, spec.serverName);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
200
290
|
if (!connected) {
|
|
201
291
|
const authConnection = state.manager.getConnection(spec.serverName);
|
|
202
292
|
if (authConnection?.status === "needs-auth") {
|
|
293
|
+
const message = `MCP server "${spec.serverName}" requires OAuth authentication. Run /mcp-auth ${spec.serverName} first.`;
|
|
203
294
|
return {
|
|
204
|
-
content: [{ type: "text" as const, text:
|
|
205
|
-
details: { error: "auth_required", server: spec.serverName },
|
|
295
|
+
content: [{ type: "text" as const, text: message }],
|
|
296
|
+
details: { error: "auth_required", server: spec.serverName, message, autoAuthAttempted },
|
|
206
297
|
};
|
|
207
298
|
}
|
|
208
299
|
const failedAgo = getFailureAgeSeconds(state, spec.serverName);
|
package/index.ts
CHANGED
|
@@ -3,12 +3,12 @@ import type { McpExtensionState } from "./state.js";
|
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import { showStatus, showTools, reconnectServers, authenticateServer, openMcpPanel } from "./commands.js";
|
|
5
5
|
import { loadMcpConfig } from "./config.js";
|
|
6
|
-
import { buildProxyDescription, createDirectToolExecutor, resolveDirectTools } from "./direct-tools.js";
|
|
6
|
+
import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.js";
|
|
7
7
|
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
|
|
8
8
|
import { loadMetadataCache } from "./metadata-cache.js";
|
|
9
9
|
import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
|
|
10
10
|
import { getConfigPathFromArgv, truncateAtWord } from "./utils.js";
|
|
11
|
-
import { initializeOAuth } from "./mcp-auth-flow.js";
|
|
11
|
+
import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.js";
|
|
12
12
|
|
|
13
13
|
export default function mcpAdapter(pi: ExtensionAPI) {
|
|
14
14
|
let state: McpExtensionState | null = null;
|
|
@@ -59,6 +59,11 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
59
59
|
prefix,
|
|
60
60
|
envRaw?.split(",").map(s => s.trim()).filter(Boolean),
|
|
61
61
|
);
|
|
62
|
+
const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(earlyConfig, earlyCache);
|
|
63
|
+
const shouldRegisterProxyTool =
|
|
64
|
+
earlyConfig.settings?.disableProxyTool !== true
|
|
65
|
+
|| directSpecs.length === 0
|
|
66
|
+
|| missingConfiguredDirectToolServers.length > 0;
|
|
62
67
|
|
|
63
68
|
for (const spec of directSpecs) {
|
|
64
69
|
pi.registerTool({
|
|
@@ -85,7 +90,10 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
85
90
|
initPromise = null;
|
|
86
91
|
|
|
87
92
|
try {
|
|
88
|
-
await
|
|
93
|
+
await Promise.all([
|
|
94
|
+
shutdownState(previousState, "session_restart"),
|
|
95
|
+
shutdownOAuth(),
|
|
96
|
+
]);
|
|
89
97
|
} catch (error) {
|
|
90
98
|
console.error("MCP: failed to shut down previous session state", error);
|
|
91
99
|
}
|
|
@@ -133,7 +141,10 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
133
141
|
initPromise = null;
|
|
134
142
|
|
|
135
143
|
try {
|
|
136
|
-
await
|
|
144
|
+
await Promise.all([
|
|
145
|
+
shutdownState(currentState, "session_shutdown"),
|
|
146
|
+
shutdownOAuth(),
|
|
147
|
+
]);
|
|
137
148
|
} catch (error) {
|
|
138
149
|
console.error("MCP: session shutdown cleanup failed", error);
|
|
139
150
|
}
|
|
@@ -207,86 +218,88 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
207
218
|
},
|
|
208
219
|
});
|
|
209
220
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
221
|
+
if (shouldRegisterProxyTool) {
|
|
222
|
+
pi.registerTool({
|
|
223
|
+
name: "mcp",
|
|
224
|
+
label: "MCP",
|
|
225
|
+
description: buildProxyDescription(earlyConfig, earlyCache, directSpecs),
|
|
226
|
+
promptSnippet: "MCP gateway - connect to MCP servers and call their tools",
|
|
227
|
+
parameters: Type.Object({
|
|
228
|
+
tool: Type.Optional(Type.String({ description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })),
|
|
229
|
+
args: Type.Optional(Type.String({ description: "Arguments as JSON string (e.g., '{\"key\": \"value\"}')" })),
|
|
230
|
+
connect: Type.Optional(Type.String({ description: "Server name to connect (lazy connect + metadata refresh)" })),
|
|
231
|
+
describe: Type.Optional(Type.String({ description: "Tool name to describe (shows parameters)" })),
|
|
232
|
+
search: Type.Optional(Type.String({ description: "Search tools by name/description" })),
|
|
233
|
+
regex: Type.Optional(Type.Boolean({ description: "Treat search as regex (default: substring match)" })),
|
|
234
|
+
includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
|
|
235
|
+
server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
|
|
236
|
+
action: Type.Optional(Type.String({ description: "Action: 'ui-messages' to retrieve prompts/intents from UI sessions" })),
|
|
237
|
+
}),
|
|
238
|
+
async execute(_toolCallId, params: {
|
|
239
|
+
tool?: string;
|
|
240
|
+
args?: string;
|
|
241
|
+
connect?: string;
|
|
242
|
+
describe?: string;
|
|
243
|
+
search?: string;
|
|
244
|
+
regex?: boolean;
|
|
245
|
+
includeSchemas?: boolean;
|
|
246
|
+
server?: string;
|
|
247
|
+
action?: string;
|
|
248
|
+
}, _signal, _onUpdate, _ctx) {
|
|
249
|
+
let parsedArgs: Record<string, unknown> | undefined;
|
|
250
|
+
if (params.args) {
|
|
251
|
+
try {
|
|
252
|
+
parsedArgs = JSON.parse(params.args);
|
|
253
|
+
if (typeof parsedArgs !== "object" || parsedArgs === null || Array.isArray(parsedArgs)) {
|
|
254
|
+
const gotType = Array.isArray(parsedArgs) ? "array" : parsedArgs === null ? "null" : typeof parsedArgs;
|
|
255
|
+
throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof SyntaxError) {
|
|
259
|
+
throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
248
262
|
}
|
|
249
|
-
throw error;
|
|
250
263
|
}
|
|
251
|
-
}
|
|
252
264
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
265
|
+
if (!state && initPromise) {
|
|
266
|
+
try {
|
|
267
|
+
state = await initPromise;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
|
|
272
|
+
details: { error: "init_failed", message },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!state) {
|
|
258
277
|
return {
|
|
259
|
-
content: [{ type: "text" as const, text:
|
|
260
|
-
details: { error: "
|
|
278
|
+
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
279
|
+
details: { error: "not_initialized" },
|
|
261
280
|
};
|
|
262
281
|
}
|
|
263
|
-
}
|
|
264
|
-
if (!state) {
|
|
265
|
-
return {
|
|
266
|
-
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
267
|
-
details: { error: "not_initialized" },
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
282
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
if (params.action === "ui-messages") {
|
|
284
|
+
return executeUiMessages(state);
|
|
285
|
+
}
|
|
286
|
+
if (params.tool) {
|
|
287
|
+
return executeCall(state, params.tool, parsedArgs, params.server);
|
|
288
|
+
}
|
|
289
|
+
if (params.connect) {
|
|
290
|
+
return executeConnect(state, params.connect);
|
|
291
|
+
}
|
|
292
|
+
if (params.describe) {
|
|
293
|
+
return executeDescribe(state, params.describe);
|
|
294
|
+
}
|
|
295
|
+
if (params.search) {
|
|
296
|
+
return executeSearch(state, params.search, params.regex, params.server, params.includeSchemas, getPiTools);
|
|
297
|
+
}
|
|
298
|
+
if (params.server) {
|
|
299
|
+
return executeList(state, params.server);
|
|
300
|
+
}
|
|
301
|
+
return executeStatus(state);
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
292
305
|
}
|
package/init.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { buildToolMetadata, totalToolCount } from "./tool-metadata.js";
|
|
|
21
21
|
import { UiResourceHandler } from "./ui-resource-handler.js";
|
|
22
22
|
import { openUrl, parallelLimit } from "./utils.js";
|
|
23
23
|
import { logger } from "./logger.js";
|
|
24
|
+
import { getMissingConfiguredDirectToolServers } from "./direct-tools.js";
|
|
24
25
|
|
|
25
26
|
const FAILURE_BACKOFF_MS = 60 * 1000;
|
|
26
27
|
|
|
@@ -89,7 +90,7 @@ export async function initializeMcp(
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
if (cache?.servers?.[name] && isServerCacheValid(cache.servers[name], definition)) {
|
|
92
|
-
const metadata = reconstructToolMetadata(name, cache.servers[name], prefix, definition
|
|
93
|
+
const metadata = reconstructToolMetadata(name, cache.servers[name], prefix, definition);
|
|
93
94
|
toolMetadata.set(name, metadata);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -151,18 +152,8 @@ export async function initializeMcp(
|
|
|
151
152
|
|
|
152
153
|
const envDirect = process.env.MCP_DIRECT_TOOLS;
|
|
153
154
|
if (envDirect !== "__none__") {
|
|
154
|
-
const missingCacheServers: string[] = [];
|
|
155
155
|
const currentCache = loadMetadataCache();
|
|
156
|
-
|
|
157
|
-
const hasDirect = definition.directTools !== undefined
|
|
158
|
-
? !!definition.directTools
|
|
159
|
-
: !!config.settings?.directTools;
|
|
160
|
-
if (!hasDirect) continue;
|
|
161
|
-
const entry = currentCache?.servers?.[name];
|
|
162
|
-
if (!entry || !isServerCacheValid(entry, definition)) {
|
|
163
|
-
missingCacheServers.push(name);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
156
|
+
const missingCacheServers = getMissingConfiguredDirectToolServers(config, currentCache);
|
|
166
157
|
|
|
167
158
|
if (missingCacheServers.length > 0) {
|
|
168
159
|
const bootstrapResults = await parallelLimit(
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -36,6 +36,9 @@ export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
|
|
|
36
36
|
// Track pending transports for auth completion
|
|
37
37
|
const pendingTransports = new Map<string, StreamableHTTPClientTransport>()
|
|
38
38
|
|
|
39
|
+
// Deduplicate concurrent authenticate() calls per server.
|
|
40
|
+
const pendingAuthentications = new Map<string, Promise<AuthStatus>>()
|
|
41
|
+
|
|
39
42
|
/**
|
|
40
43
|
* Generate a cryptographically secure random state parameter.
|
|
41
44
|
*/
|
|
@@ -182,56 +185,74 @@ export async function authenticate(
|
|
|
182
185
|
serverUrl: string,
|
|
183
186
|
definition?: ServerEntry,
|
|
184
187
|
): Promise<AuthStatus> {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// If no auth URL needed, already authenticated
|
|
189
|
-
if (!authorizationUrl) {
|
|
190
|
-
return "authenticated"
|
|
188
|
+
const inFlight = pendingAuthentications.get(serverName)
|
|
189
|
+
if (inFlight) {
|
|
190
|
+
return inFlight
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
throw new Error("OAuth state not found - this should not happen")
|
|
197
|
-
}
|
|
193
|
+
const operation = (async (): Promise<AuthStatus> => {
|
|
194
|
+
// Start auth flow
|
|
195
|
+
const { authorizationUrl } = await startAuth(serverName, serverUrl, definition)
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
// If no auth URL needed, already authenticated
|
|
198
|
+
if (!authorizationUrl) {
|
|
199
|
+
return "authenticated"
|
|
200
|
+
}
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
console.warn(`MCP Auth: Failed to open browser for ${serverName}`, { error })
|
|
208
|
-
throw new Error(
|
|
209
|
-
`Could not open browser. Please open this URL manually: ${authorizationUrl}`
|
|
210
|
-
)
|
|
211
|
-
}
|
|
202
|
+
// Get the state that was already generated and stored in startAuth()
|
|
203
|
+
const oauthState = await getOAuthState(serverName)
|
|
204
|
+
if (!oauthState) {
|
|
205
|
+
throw new Error("OAuth state not found - this should not happen")
|
|
206
|
+
}
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
208
|
+
// Register the callback BEFORE opening the browser
|
|
209
|
+
const callbackPromise = waitForCallback(oauthState)
|
|
210
|
+
|
|
211
|
+
// Open browser
|
|
212
|
+
console.log(`MCP Auth: Opening browser for ${serverName}`)
|
|
213
|
+
try {
|
|
214
|
+
await open(authorizationUrl)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.warn(`MCP Auth: Failed to open browser for ${serverName}`, { error })
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Could not open browser. Please open this URL manually: ${authorizationUrl}`,
|
|
219
|
+
{ cause: error },
|
|
220
|
+
)
|
|
221
|
+
}
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
try {
|
|
224
|
+
// Wait for callback
|
|
225
|
+
const code = await callbackPromise
|
|
226
|
+
|
|
227
|
+
// Validate state
|
|
228
|
+
const storedState = await getOAuthState(serverName)
|
|
229
|
+
if (storedState !== oauthState) {
|
|
230
|
+
await clearOAuthState(serverName)
|
|
231
|
+
throw new Error("OAuth state mismatch - potential CSRF attack")
|
|
232
|
+
}
|
|
220
233
|
await clearOAuthState(serverName)
|
|
221
|
-
|
|
234
|
+
|
|
235
|
+
// Complete the auth
|
|
236
|
+
return await completeAuth(serverName, code)
|
|
237
|
+
} catch (error) {
|
|
238
|
+
cancelPendingCallback(oauthState)
|
|
239
|
+
const pendingTransport = pendingTransports.get(serverName)
|
|
240
|
+
if (pendingTransport) {
|
|
241
|
+
pendingTransports.delete(serverName)
|
|
242
|
+
await pendingTransport.close().catch(() => {})
|
|
243
|
+
}
|
|
244
|
+
throw error
|
|
222
245
|
}
|
|
223
|
-
|
|
246
|
+
})()
|
|
224
247
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
await pendingTransport.close().catch(() => {})
|
|
248
|
+
pendingAuthentications.set(serverName, operation)
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
return await operation
|
|
252
|
+
} finally {
|
|
253
|
+
if (pendingAuthentications.get(serverName) === operation) {
|
|
254
|
+
pendingAuthentications.delete(serverName)
|
|
233
255
|
}
|
|
234
|
-
throw error
|
|
235
256
|
}
|
|
236
257
|
}
|
|
237
258
|
|
package/mcp-callback-server.ts
CHANGED
|
@@ -155,9 +155,13 @@ export async function ensureCallbackServer(options: EnsureCallbackServerOptions
|
|
|
155
155
|
if (server) {
|
|
156
156
|
if (!strictPort || getOAuthCallbackPort() === configuredPort) return
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
if (pendingAuths.size > 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`OAuth callback server is running on port ${getOAuthCallbackPort()}, but strict callback port ${configuredPort} is required and cannot be switched while authorizations are pending`
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await stopCallbackServer()
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
const preferredPort = configuredPort
|
|
@@ -180,6 +184,7 @@ export async function ensureCallbackServer(options: EnsureCallbackServerOptions
|
|
|
180
184
|
})
|
|
181
185
|
|
|
182
186
|
server = candidateServer
|
|
187
|
+
server.unref()
|
|
183
188
|
setOAuthCallbackPort(candidatePort)
|
|
184
189
|
return
|
|
185
190
|
} catch (error) {
|
|
@@ -257,7 +262,7 @@ export async function stopCallbackServer(): Promise<void> {
|
|
|
257
262
|
const pendingList = Array.from(pendingAuths.entries())
|
|
258
263
|
pendingAuths.clear()
|
|
259
264
|
setTimeout(() => {
|
|
260
|
-
for (const [
|
|
265
|
+
for (const [, pending] of pendingList) {
|
|
261
266
|
clearTimeout(pending.timeout)
|
|
262
267
|
pending.reject(new Error("OAuth callback server stopped"))
|
|
263
268
|
}
|
package/mcp-panel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import { isToolExcluded } from "./types.js";
|
|
2
3
|
import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.js";
|
|
3
4
|
import { resourceNameToToolName } from "./resource-tools.js";
|
|
4
5
|
import type { MetadataCache, ServerCacheEntry, CachedTool } from "./metadata-cache.js";
|
|
@@ -93,6 +94,8 @@ interface ServerState {
|
|
|
93
94
|
expanded: boolean;
|
|
94
95
|
source: "user" | "project" | "import";
|
|
95
96
|
importKind?: string;
|
|
97
|
+
excludeTools?: string[];
|
|
98
|
+
exposeResources: boolean;
|
|
96
99
|
connectionStatus: ConnectionStatus;
|
|
97
100
|
tools: ToolState[];
|
|
98
101
|
hasCachedData: boolean;
|
|
@@ -105,6 +108,7 @@ interface VisibleItem {
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
class McpPanel {
|
|
111
|
+
private prefix: "server" | "none" | "short";
|
|
108
112
|
private servers: ServerState[] = [];
|
|
109
113
|
private cursorIndex = 0;
|
|
110
114
|
private nameQuery = "";
|
|
@@ -132,6 +136,7 @@ class McpPanel {
|
|
|
132
136
|
private done: (result: McpPanelResult) => void,
|
|
133
137
|
) {
|
|
134
138
|
this.tui = tui;
|
|
139
|
+
this.prefix = config.settings?.toolPrefix ?? "server";
|
|
135
140
|
|
|
136
141
|
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
137
142
|
const prov = provenance.get(serverName);
|
|
@@ -148,6 +153,10 @@ class McpPanel {
|
|
|
148
153
|
const tools: ToolState[] = [];
|
|
149
154
|
if (serverCache) {
|
|
150
155
|
for (const tool of serverCache.tools ?? []) {
|
|
156
|
+
if (isToolExcluded(tool.name, serverName, this.prefix, definition.excludeTools)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
const isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(tool.name));
|
|
152
161
|
tools.push({
|
|
153
162
|
name: tool.name,
|
|
@@ -160,6 +169,10 @@ class McpPanel {
|
|
|
160
169
|
if (definition.exposeResources !== false) {
|
|
161
170
|
for (const resource of serverCache.resources ?? []) {
|
|
162
171
|
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
172
|
+
if (isToolExcluded(baseName, serverName, this.prefix, definition.excludeTools)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
163
176
|
const isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(baseName));
|
|
164
177
|
const ct: CachedTool = { name: baseName, description: resource.description };
|
|
165
178
|
tools.push({
|
|
@@ -180,6 +193,8 @@ class McpPanel {
|
|
|
180
193
|
expanded: false,
|
|
181
194
|
source: prov?.kind ?? "user",
|
|
182
195
|
importKind: prov?.importKind,
|
|
196
|
+
excludeTools: definition.excludeTools,
|
|
197
|
+
exposeResources: definition.exposeResources !== false,
|
|
183
198
|
connectionStatus: status,
|
|
184
199
|
tools,
|
|
185
200
|
hasCachedData: !!serverCache,
|
|
@@ -385,8 +400,10 @@ class McpPanel {
|
|
|
385
400
|
server.hasCachedData = true;
|
|
386
401
|
}
|
|
387
402
|
this.tui.requestRender();
|
|
388
|
-
}).catch(() => {
|
|
403
|
+
}).catch((error) => {
|
|
389
404
|
server.connectionStatus = "failed";
|
|
405
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
406
|
+
this.authNotice = `Reconnect failed for ${server.name}: ${message}`;
|
|
390
407
|
this.tui.requestRender();
|
|
391
408
|
});
|
|
392
409
|
return;
|
|
@@ -477,6 +494,10 @@ class McpPanel {
|
|
|
477
494
|
|
|
478
495
|
const newTools: ToolState[] = [];
|
|
479
496
|
for (const tool of entry.tools ?? []) {
|
|
497
|
+
if (isToolExcluded(tool.name, server.name, this.prefix, server.excludeTools)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
480
501
|
const prev = existingState.get(tool.name);
|
|
481
502
|
const isDirect = prev !== undefined ? prev : false;
|
|
482
503
|
newTools.push({
|
|
@@ -488,18 +509,24 @@ class McpPanel {
|
|
|
488
509
|
});
|
|
489
510
|
}
|
|
490
511
|
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
isDirect
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
512
|
+
if (server.exposeResources) {
|
|
513
|
+
for (const resource of entry.resources ?? []) {
|
|
514
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
515
|
+
if (isToolExcluded(baseName, server.name, this.prefix, server.excludeTools)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const prev = existingState.get(baseName);
|
|
520
|
+
const isDirect = prev !== undefined ? prev : false;
|
|
521
|
+
const ct: CachedTool = { name: baseName, description: resource.description };
|
|
522
|
+
newTools.push({
|
|
523
|
+
name: baseName,
|
|
524
|
+
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
525
|
+
isDirect,
|
|
526
|
+
wasDirect: prev !== undefined ? server.tools.find((t) => t.name === baseName)?.wasDirect ?? false : false,
|
|
527
|
+
estimatedTokens: estimateTokens(ct),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
503
530
|
}
|
|
504
531
|
|
|
505
532
|
server.tools = newTools;
|
package/metadata-cache.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
7
7
|
import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.js";
|
|
8
|
-
import { formatToolName } from "./types.js";
|
|
8
|
+
import { formatToolName, isToolExcluded } from "./types.js";
|
|
9
9
|
import { resourceNameToToolName } from "./resource-tools.js";
|
|
10
10
|
import { extractToolUiStreamMode } from "./utils.js";
|
|
11
11
|
|
|
@@ -95,6 +95,7 @@ export function computeServerHash(definition: ServerEntry): string {
|
|
|
95
95
|
bearerToken: definition.bearerToken,
|
|
96
96
|
bearerTokenEnv: definition.bearerTokenEnv,
|
|
97
97
|
exposeResources: definition.exposeResources,
|
|
98
|
+
excludeTools: definition.excludeTools,
|
|
98
99
|
};
|
|
99
100
|
const normalized = stableStringify(identity);
|
|
100
101
|
return createHash("sha256").update(normalized).digest("hex");
|
|
@@ -115,12 +116,16 @@ export function reconstructToolMetadata(
|
|
|
115
116
|
serverName: string,
|
|
116
117
|
entry: ServerCacheEntry,
|
|
117
118
|
prefix: "server" | "none" | "short",
|
|
118
|
-
exposeResources
|
|
119
|
+
definition: Pick<ServerEntry, "exposeResources" | "excludeTools">
|
|
119
120
|
): ToolMetadata[] {
|
|
120
121
|
const metadata: ToolMetadata[] = [];
|
|
121
122
|
|
|
122
123
|
for (const tool of entry.tools ?? []) {
|
|
123
124
|
if (!tool?.name) continue;
|
|
125
|
+
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
metadata.push({
|
|
125
130
|
name: formatToolName(tool.name, serverName, prefix),
|
|
126
131
|
originalName: tool.name,
|
|
@@ -131,10 +136,14 @@ export function reconstructToolMetadata(
|
|
|
131
136
|
});
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
if (exposeResources !== false) {
|
|
139
|
+
if (definition.exposeResources !== false) {
|
|
135
140
|
for (const resource of entry.resources ?? []) {
|
|
136
141
|
if (!resource?.name || !resource?.uri) continue;
|
|
137
142
|
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
143
|
+
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
metadata.push({
|
|
139
148
|
name: formatToolName(baseName, serverName, prefix),
|
|
140
149
|
originalName: baseName,
|
package/package.json
CHANGED
package/proxy-modes.ts
CHANGED
|
@@ -7,9 +7,52 @@ import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from ".
|
|
|
7
7
|
import { transformMcpContent } from "./tool-registrar.js";
|
|
8
8
|
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
|
|
9
9
|
import { truncateAtWord } from "./utils.js";
|
|
10
|
+
import { authenticate, supportsOAuth } from "./mcp-auth-flow.js";
|
|
10
11
|
|
|
11
12
|
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
12
13
|
|
|
14
|
+
type AutoAuthResult =
|
|
15
|
+
| { status: "skipped" }
|
|
16
|
+
| { status: "success" }
|
|
17
|
+
| { status: "failed"; message: string };
|
|
18
|
+
|
|
19
|
+
function getAuthRequiredMessage(serverName: string): string {
|
|
20
|
+
return `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function attemptAutoAuth(
|
|
24
|
+
state: McpExtensionState,
|
|
25
|
+
serverName: string,
|
|
26
|
+
): Promise<AutoAuthResult> {
|
|
27
|
+
if (state.config.settings?.autoAuth !== true) {
|
|
28
|
+
return { status: "skipped" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const definition = state.config.mcpServers[serverName];
|
|
32
|
+
if (!definition || !supportsOAuth(definition) || !definition.url) {
|
|
33
|
+
return { status: "skipped" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const grantType = definition.oauth?.grantType ?? "authorization_code";
|
|
37
|
+
if (!state.ui && grantType !== "client_credentials") {
|
|
38
|
+
return {
|
|
39
|
+
status: "failed",
|
|
40
|
+
message: `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} in an interactive session.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await authenticate(serverName, definition.url, definition);
|
|
46
|
+
return { status: "success" };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
+
return {
|
|
50
|
+
status: "failed",
|
|
51
|
+
message: `OAuth authentication failed for "${serverName}": ${message}. Run /mcp-auth ${serverName} first.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
export function executeUiMessages(state: McpExtensionState): ProxyToolResult {
|
|
14
57
|
const sessions = state.completedUiSessions;
|
|
15
58
|
|
|
@@ -376,12 +419,26 @@ export async function executeConnect(state: McpExtensionState, serverName: strin
|
|
|
376
419
|
if (state.ui) {
|
|
377
420
|
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
378
421
|
}
|
|
379
|
-
|
|
422
|
+
let connection = await state.manager.connect(serverName, definition);
|
|
380
423
|
if (connection.status === "needs-auth") {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
424
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
425
|
+
if (autoAuth.status === "failed") {
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
428
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (autoAuth.status === "success") {
|
|
432
|
+
await state.manager.close(serverName);
|
|
433
|
+
connection = await state.manager.connect(serverName, definition);
|
|
434
|
+
}
|
|
435
|
+
if (connection.status === "needs-auth") {
|
|
436
|
+
const message = getAuthRequiredMessage(serverName);
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: "text" as const, text: message }],
|
|
439
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
385
442
|
}
|
|
386
443
|
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
387
444
|
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
@@ -409,6 +466,7 @@ export async function executeCall(
|
|
|
409
466
|
): Promise<ProxyToolResult> {
|
|
410
467
|
let serverName: string | undefined = serverOverride;
|
|
411
468
|
let toolMeta: ToolMetadata | undefined;
|
|
469
|
+
let autoAuthAttempted = false;
|
|
412
470
|
const prefixMode = state.config.settings?.toolPrefix ?? "server";
|
|
413
471
|
|
|
414
472
|
if (serverName && !state.config.mcpServers[serverName]) {
|
|
@@ -438,17 +496,48 @@ export async function executeCall(
|
|
|
438
496
|
} else {
|
|
439
497
|
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
440
498
|
if (needsAuthConnection?.status === "needs-auth") {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
499
|
+
if (!autoAuthAttempted) {
|
|
500
|
+
autoAuthAttempted = true;
|
|
501
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
502
|
+
if (autoAuth.status === "failed") {
|
|
503
|
+
return {
|
|
504
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
505
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (autoAuth.status === "success") {
|
|
509
|
+
await state.manager.close(serverName);
|
|
510
|
+
state.failureTracker.delete(serverName);
|
|
511
|
+
const connectedAfterAuth = await lazyConnect(state, serverName);
|
|
512
|
+
if (connectedAfterAuth) {
|
|
513
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
514
|
+
if (!toolMeta) {
|
|
515
|
+
return {
|
|
516
|
+
content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect.` }],
|
|
517
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
|
|
525
|
+
const message = getAuthRequiredMessage(serverName);
|
|
526
|
+
return {
|
|
527
|
+
content: [{ type: "text" as const, text: message }],
|
|
528
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
529
|
+
};
|
|
530
|
+
}
|
|
445
531
|
}
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
532
|
+
|
|
533
|
+
if (!toolMeta) {
|
|
534
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
535
|
+
if (failedAgo !== null) {
|
|
536
|
+
return {
|
|
537
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
538
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
539
|
+
};
|
|
540
|
+
}
|
|
452
541
|
}
|
|
453
542
|
}
|
|
454
543
|
}
|
|
@@ -462,9 +551,27 @@ export async function executeCall(
|
|
|
462
551
|
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
463
552
|
|
|
464
553
|
for (const { name: configuredServer } of candidates) {
|
|
554
|
+
const existingConnection = state.manager.getConnection(configuredServer);
|
|
465
555
|
const failedAgo = getFailureAgeSeconds(state, configuredServer);
|
|
466
|
-
if (failedAgo !== null) continue;
|
|
467
|
-
|
|
556
|
+
if (failedAgo !== null && existingConnection?.status !== "needs-auth") continue;
|
|
557
|
+
|
|
558
|
+
let connected = await lazyConnect(state, configuredServer);
|
|
559
|
+
if (!connected && state.manager.getConnection(configuredServer)?.status === "needs-auth" && !autoAuthAttempted) {
|
|
560
|
+
autoAuthAttempted = true;
|
|
561
|
+
const autoAuth = await attemptAutoAuth(state, configuredServer);
|
|
562
|
+
if (autoAuth.status === "failed") {
|
|
563
|
+
return {
|
|
564
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
565
|
+
details: { mode: "call", error: "auth_required", server: configuredServer, message: autoAuth.message },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (autoAuth.status === "success") {
|
|
569
|
+
await state.manager.close(configuredServer);
|
|
570
|
+
state.failureTracker.delete(configuredServer);
|
|
571
|
+
connected = await lazyConnect(state, configuredServer);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
468
575
|
if (!connected) continue;
|
|
469
576
|
if (!prefixMatchedServer) prefixMatchedServer = configuredServer;
|
|
470
577
|
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
@@ -492,10 +599,29 @@ export async function executeCall(
|
|
|
492
599
|
|
|
493
600
|
let connection = state.manager.getConnection(serverName);
|
|
494
601
|
if (connection?.status === "needs-auth") {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
602
|
+
if (!autoAuthAttempted) {
|
|
603
|
+
autoAuthAttempted = true;
|
|
604
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
605
|
+
if (autoAuth.status === "failed") {
|
|
606
|
+
return {
|
|
607
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
608
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (autoAuth.status === "success") {
|
|
612
|
+
await state.manager.close(serverName);
|
|
613
|
+
state.failureTracker.delete(serverName);
|
|
614
|
+
connection = state.manager.getConnection(serverName);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (connection?.status === "needs-auth") {
|
|
619
|
+
const message = getAuthRequiredMessage(serverName);
|
|
620
|
+
return {
|
|
621
|
+
content: [{ type: "text" as const, text: message }],
|
|
622
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
623
|
+
};
|
|
624
|
+
}
|
|
499
625
|
}
|
|
500
626
|
if (!connection || connection.status !== "connected") {
|
|
501
627
|
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
@@ -520,10 +646,28 @@ export async function executeCall(
|
|
|
520
646
|
}
|
|
521
647
|
connection = await state.manager.connect(serverName, definition);
|
|
522
648
|
if (connection.status === "needs-auth") {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
649
|
+
if (!autoAuthAttempted) {
|
|
650
|
+
autoAuthAttempted = true;
|
|
651
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
652
|
+
if (autoAuth.status === "failed") {
|
|
653
|
+
return {
|
|
654
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
655
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (autoAuth.status === "success") {
|
|
659
|
+
await state.manager.close(serverName);
|
|
660
|
+
connection = await state.manager.connect(serverName, definition);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (connection.status === "needs-auth") {
|
|
665
|
+
const message = getAuthRequiredMessage(serverName);
|
|
666
|
+
return {
|
|
667
|
+
content: [{ type: "text" as const, text: message }],
|
|
668
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
669
|
+
};
|
|
670
|
+
}
|
|
527
671
|
}
|
|
528
672
|
state.failureTracker.delete(serverName);
|
|
529
673
|
updateServerMetadata(state, serverName);
|
package/tool-metadata.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
2
2
|
import type { McpExtensionState } from "./state.js";
|
|
3
3
|
import type { ToolMetadata, McpTool, McpResource, ServerEntry } from "./types.js";
|
|
4
|
-
import { formatToolName } from "./types.js";
|
|
4
|
+
import { formatToolName, isToolExcluded } from "./types.js";
|
|
5
5
|
import { resourceNameToToolName } from "./resource-tools.js";
|
|
6
6
|
import { extractToolUiStreamMode } from "./utils.js";
|
|
7
7
|
|
|
@@ -20,6 +20,10 @@ export function buildToolMetadata(
|
|
|
20
20
|
failedTools.push("(unnamed)");
|
|
21
21
|
continue;
|
|
22
22
|
}
|
|
23
|
+
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
let uiResourceUri: string | undefined;
|
|
24
28
|
try {
|
|
25
29
|
uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
|
|
@@ -39,6 +43,10 @@ export function buildToolMetadata(
|
|
|
39
43
|
if (definition.exposeResources !== false) {
|
|
40
44
|
for (const resource of resources) {
|
|
41
45
|
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
46
|
+
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
metadata.push({
|
|
43
51
|
name: formatToolName(baseName, serverName, prefix),
|
|
44
52
|
originalName: baseName,
|
package/types.ts
CHANGED
|
@@ -305,6 +305,8 @@ export interface ServerEntry {
|
|
|
305
305
|
exposeResources?: boolean;
|
|
306
306
|
// Direct tool registration
|
|
307
307
|
directTools?: boolean | string[];
|
|
308
|
+
// Exclude specific MCP tools/resources by original or prefixed name
|
|
309
|
+
excludeTools?: string[];
|
|
308
310
|
// Debug
|
|
309
311
|
debug?: boolean; // Show server stderr (default: false)
|
|
310
312
|
}
|
|
@@ -314,6 +316,8 @@ export interface McpSettings {
|
|
|
314
316
|
toolPrefix?: "server" | "none" | "short";
|
|
315
317
|
idleTimeout?: number; // minutes, default 10, 0 to disable
|
|
316
318
|
directTools?: boolean;
|
|
319
|
+
disableProxyTool?: boolean;
|
|
320
|
+
autoAuth?: boolean;
|
|
317
321
|
}
|
|
318
322
|
|
|
319
323
|
// Root config
|
|
@@ -391,3 +395,32 @@ export function formatToolName(
|
|
|
391
395
|
const p = getServerPrefix(serverName, prefix);
|
|
392
396
|
return p ? `${p}_${toolName}` : toolName;
|
|
393
397
|
}
|
|
398
|
+
|
|
399
|
+
function normalizeToolName(value: string): string {
|
|
400
|
+
return value.replace(/-/g, "_");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function isToolExcluded(
|
|
404
|
+
toolName: string,
|
|
405
|
+
serverName: string,
|
|
406
|
+
prefix: "server" | "none" | "short",
|
|
407
|
+
excludeTools?: unknown
|
|
408
|
+
): boolean {
|
|
409
|
+
if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
|
|
410
|
+
|
|
411
|
+
const candidates = new Set<string>([
|
|
412
|
+
normalizeToolName(toolName),
|
|
413
|
+
normalizeToolName(formatToolName(toolName, serverName, prefix)),
|
|
414
|
+
normalizeToolName(formatToolName(toolName, serverName, "server")),
|
|
415
|
+
normalizeToolName(formatToolName(toolName, serverName, "short")),
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
for (const excluded of excludeTools) {
|
|
419
|
+
if (typeof excluded !== "string") continue;
|
|
420
|
+
if (candidates.has(normalizeToolName(excluded))) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return false;
|
|
426
|
+
}
|