@sammorrowdrums/mcpi-ext 0.2.0 → 1.0.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/LICENSE +21 -0
- package/README.md +137 -47
- package/dist/code-mode/eligibility.d.ts +35 -0
- package/dist/code-mode/eligibility.js +64 -0
- package/dist/code-mode/executor.d.ts +53 -0
- package/dist/code-mode/executor.js +185 -0
- package/dist/code-mode/index.d.ts +107 -0
- package/dist/code-mode/index.js +267 -0
- package/dist/code-mode/isolated-vm.d.ts +65 -0
- package/dist/code-mode/isolated-vm.js +116 -0
- package/dist/code-mode/tools.d.ts +48 -0
- package/dist/code-mode/tools.js +73 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.js +205 -0
- package/dist/docker-e2e.d.ts +54 -0
- package/dist/docker-e2e.js +234 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +227 -0
- package/dist/mcp/call-tool-result.d.ts +10 -0
- package/dist/mcp/call-tool-result.js +70 -0
- package/dist/mcp/client-factory.d.ts +59 -0
- package/dist/mcp/client-factory.js +76 -0
- package/dist/mcp/client-manager.d.ts +88 -0
- package/dist/mcp/client-manager.js +307 -0
- package/dist/mcp/config-loader.d.ts +7 -0
- package/dist/mcp/config-loader.js +42 -0
- package/dist/mcp/config.d.ts +56 -0
- package/dist/mcp/config.js +42 -0
- package/dist/mcp/gateway-defaults.d.ts +14 -0
- package/dist/mcp/gateway-defaults.js +21 -0
- package/dist/mcp/host-approval.d.ts +16 -0
- package/dist/mcp/host-approval.js +19 -0
- package/dist/mcp/host-elicitation.d.ts +19 -0
- package/dist/mcp/host-elicitation.js +94 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/policy.d.ts +314 -0
- package/dist/mcp/policy.js +848 -0
- package/dist/routing/facilities.d.ts +109 -0
- package/dist/routing/facilities.js +175 -0
- package/dist/routing/format.d.ts +24 -0
- package/dist/routing/format.js +64 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/seam.d.ts +35 -0
- package/dist/routing/seam.js +28 -0
- package/dist/routing/tripwire.d.ts +42 -0
- package/dist/routing/tripwire.js +80 -0
- package/dist/skills/discover.d.ts +12 -0
- package/dist/skills/discover.js +85 -0
- package/dist/skills/format.d.ts +13 -0
- package/dist/skills/format.js +42 -0
- package/dist/skills/index.d.ts +6 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/load-skill-tool.d.ts +56 -0
- package/dist/skills/load-skill-tool.js +183 -0
- package/dist/skills/mcp-tool-proxy.d.ts +7 -0
- package/dist/skills/mcp-tool-proxy.js +41 -0
- package/dist/skills/sep2640/client.d.ts +91 -0
- package/dist/skills/sep2640/client.js +160 -0
- package/dist/skills/sep2640/discover.d.ts +33 -0
- package/dist/skills/sep2640/discover.js +82 -0
- package/dist/skills/sep2640/index.d.ts +13 -0
- package/dist/skills/sep2640/index.js +13 -0
- package/dist/skills/sep2640/integrity.d.ts +58 -0
- package/dist/skills/sep2640/integrity.js +185 -0
- package/dist/skills/sep2640/load.d.ts +61 -0
- package/dist/skills/sep2640/load.js +90 -0
- package/dist/skills/sep2640/protocol.d.ts +146 -0
- package/dist/skills/sep2640/protocol.js +214 -0
- package/dist/skills/sep2640/spec.d.ts +76 -0
- package/dist/skills/sep2640/spec.js +88 -0
- package/dist/skills/skill-registry.d.ts +85 -0
- package/dist/skills/skill-registry.js +85 -0
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +20 -0
- package/dist/tool-cli/format.js +73 -0
- package/dist/tool-cli/index.d.ts +7 -0
- package/dist/tool-cli/index.js +5 -0
- package/dist/tool-cli/provider.d.ts +21 -0
- package/dist/tool-cli/provider.js +114 -0
- package/dist/tool-cli/startup.d.ts +22 -0
- package/dist/tool-cli/startup.js +47 -0
- package/package.json +78 -26
- package/.dockerignore +0 -6
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/ci.yml +0 -50
- package/.hadolint.yaml +0 -6
- package/.prettierignore +0 -4
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -118
- package/DECISIONS.md +0 -99
- package/Dockerfile +0 -39
- package/aube-lock.yaml +0 -4146
- package/docs/code-mode.md +0 -38
- package/docs/skills.md +0 -57
- package/docs/tool-cli.md +0 -54
- package/eslint.config.js +0 -27
- package/images/banner.webp +0 -0
- package/images/code-c-maude.webp +0 -0
- package/images/code-mode-histogram.png +0 -0
- package/images/nuclear-mcp-football.webp +0 -0
- package/images/skills-enabling-mcp-tools.png +0 -0
- package/images/the-skill-dealer.webp +0 -0
- package/images/tool-cli-grep.png +0 -0
- package/mise.toml +0 -53
- package/scripts/docker-entrypoint.sh +0 -29
- package/scripts/test-echo-server.mjs +0 -28
- package/scripts/test-mcp-integration.mjs +0 -59
- package/scripts/test-tool-cli-smoke.mjs +0 -93
- package/src/code-mode/code-mode-integration.test.ts +0 -135
- package/src/code-mode/eligibility.test.ts +0 -60
- package/src/code-mode/eligibility.ts +0 -16
- package/src/code-mode/executor.test.ts +0 -168
- package/src/code-mode/executor.ts +0 -163
- package/src/code-mode/index.ts +0 -208
- package/src/code-mode/tools.ts +0 -110
- package/src/code-mode/type-hints.test.ts +0 -187
- package/src/code-mode/type-hints.ts +0 -249
- package/src/docker-e2e.test.ts +0 -195
- package/src/docker-e2e.ts +0 -277
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -179
- package/src/mcp/client-manager.test.ts +0 -250
- package/src/mcp/client-manager.ts +0 -199
- package/src/mcp/config-loader.test.ts +0 -115
- package/src/mcp/config-loader.ts +0 -46
- package/src/mcp/config.ts +0 -40
- package/src/mcp/index.ts +0 -4
- package/src/skills/discover.ts +0 -81
- package/src/skills/format.test.ts +0 -70
- package/src/skills/format.ts +0 -42
- package/src/skills/index.ts +0 -9
- package/src/skills/load-skill-tool.ts +0 -136
- package/src/skills/mcp-tool-proxy.ts +0 -143
- package/src/skills/skill-integration.test.ts +0 -139
- package/src/skills/skill-registry.test.ts +0 -83
- package/src/skills/skill-registry.ts +0 -69
- package/src/test-servers/weather-server.ts +0 -171
- package/src/test-servers/weather-stdio.ts +0 -11
- package/src/tool-cli/format.test.ts +0 -33
- package/src/tool-cli/format.ts +0 -56
- package/src/tool-cli/index.ts +0 -3
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Client, SUPPORTED_PROTOCOL_VERSIONS, } from "@modelcontextprotocol/client";
|
|
2
|
+
import { SKILLS_EXTENSION_NAME, SKILLS_EXTENSION_REVISION, SKILLS_EXTENSION_STATUS, } from "../skills/sep2640/spec.js";
|
|
3
|
+
export const MCP_CLIENT_IDENTITY = {
|
|
4
|
+
name: "@sammorrowdrums/mcpi-ext",
|
|
5
|
+
version: "1.0.0",
|
|
6
|
+
};
|
|
7
|
+
export const MCP_CLIENT_POLICY = {
|
|
8
|
+
listMaxPages: 64,
|
|
9
|
+
maxInputRequiredRounds: 4,
|
|
10
|
+
probeTimeoutMs: 5_000,
|
|
11
|
+
requestTimeoutMs: 60_000,
|
|
12
|
+
maxTotalTimeoutMs: 10 * 60_000,
|
|
13
|
+
};
|
|
14
|
+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS = [
|
|
15
|
+
"2026-07-28",
|
|
16
|
+
...SUPPORTED_PROTOCOL_VERSIONS,
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Creates every MCP client with the same identity, negotiated protocol policy,
|
|
20
|
+
* capabilities, pagination limit, and SDK-managed input_required handling.
|
|
21
|
+
*/
|
|
22
|
+
export function createMcpClient(options) {
|
|
23
|
+
const client = new Client(MCP_CLIENT_IDENTITY, {
|
|
24
|
+
supportedProtocolVersions: [...MCP_SUPPORTED_PROTOCOL_VERSIONS],
|
|
25
|
+
capabilities: {
|
|
26
|
+
elicitation: { form: {} },
|
|
27
|
+
// Per SEP-2133 the extension is offered here, at initialize, and the
|
|
28
|
+
// server answers with its own settings. Every later skills request
|
|
29
|
+
// re-reads that answer instead of trusting a cached yes.
|
|
30
|
+
extensions: options.skillsExtension ? { [SKILLS_EXTENSION_NAME]: {} } : {},
|
|
31
|
+
},
|
|
32
|
+
versionNegotiation: {
|
|
33
|
+
mode: "auto",
|
|
34
|
+
probe: {
|
|
35
|
+
timeoutMs: MCP_CLIENT_POLICY.probeTimeoutMs,
|
|
36
|
+
maxRetries: 0,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
inputRequired: {
|
|
40
|
+
autoFulfill: true,
|
|
41
|
+
maxRounds: MCP_CLIENT_POLICY.maxInputRequiredRounds,
|
|
42
|
+
},
|
|
43
|
+
listChanged: options.listChanged,
|
|
44
|
+
listMaxPages: MCP_CLIENT_POLICY.listMaxPages,
|
|
45
|
+
defaultCacheTtlMs: 0,
|
|
46
|
+
});
|
|
47
|
+
client.setRequestHandler("elicitation/create", (request) => options.elicitation.elicit(request.params));
|
|
48
|
+
return client;
|
|
49
|
+
}
|
|
50
|
+
export function getMcpClientDiagnostics(client, options = {}) {
|
|
51
|
+
// Diagnostics run inside `connectOne`, so this must never be able to fail a
|
|
52
|
+
// connection. `getServerCapabilities` is absent on minimal client doubles and
|
|
53
|
+
// returns undefined before initialize completes; either way an unavailable
|
|
54
|
+
// capability is reported as "not declared", never thrown.
|
|
55
|
+
const serverCapabilities = typeof client.getServerCapabilities === "function" ? client.getServerCapabilities() : undefined;
|
|
56
|
+
const declared = serverCapabilities?.extensions?.[SKILLS_EXTENSION_NAME];
|
|
57
|
+
const capability = declared !== undefined && declared !== null && typeof declared === "object"
|
|
58
|
+
? declared
|
|
59
|
+
: undefined;
|
|
60
|
+
return {
|
|
61
|
+
protocolVersion: typeof client.getNegotiatedProtocolVersion === "function"
|
|
62
|
+
? client.getNegotiatedProtocolVersion()
|
|
63
|
+
: undefined,
|
|
64
|
+
protocolEra: client.getProtocolEra(),
|
|
65
|
+
discoverResult: client.getDiscoverResult(),
|
|
66
|
+
serverImplementation: typeof client.getServerVersion === "function" ? client.getServerVersion() : undefined,
|
|
67
|
+
serverCapabilities,
|
|
68
|
+
skillsExtension: {
|
|
69
|
+
requested: options.skillsExtensionRequested === true,
|
|
70
|
+
revision: SKILLS_EXTENSION_REVISION,
|
|
71
|
+
status: SKILLS_EXTENSION_STATUS,
|
|
72
|
+
serverDeclared: capability !== undefined,
|
|
73
|
+
serverCapability: capability,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Client, type ReadResourceResult, type Resource, type ResourceTemplateType, type Tool, type Transport } from "@modelcontextprotocol/client";
|
|
2
|
+
import { type DirectoryReadResult, type SkillsGetResult, type SkillsListResult } from "../skills/sep2640/protocol.js";
|
|
3
|
+
import { type TerminalCallToolResult } from "./call-tool-result.js";
|
|
4
|
+
import { type CreateMcpClientOptions, type McpClientDiagnostics, type McpElicitationHandler } from "./client-factory.js";
|
|
5
|
+
import type { McpConfig, ServerConfig } from "./config.js";
|
|
6
|
+
/** A lossless MCP tool as discovered from a server, tagged with its origin. */
|
|
7
|
+
export type McpTool = Tool & {
|
|
8
|
+
/** Which configured server this tool came from. */
|
|
9
|
+
serverName: string;
|
|
10
|
+
};
|
|
11
|
+
export interface McpClientManagerOptions {
|
|
12
|
+
elicitation?: McpElicitationHandler;
|
|
13
|
+
clientFactory?: (options: CreateMcpClientOptions) => Client;
|
|
14
|
+
transportFactory?: (config: ServerConfig) => Transport;
|
|
15
|
+
/**
|
|
16
|
+
* Advertise the draft skills extension (SEP-2640) at initialize.
|
|
17
|
+
*
|
|
18
|
+
* Off by default; the host turns it on from config. When off, no connection
|
|
19
|
+
* this manager opens mentions the extension at all.
|
|
20
|
+
*/
|
|
21
|
+
skillsExtension?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Manages connections to multiple MCP servers and aggregates their tools.
|
|
25
|
+
*
|
|
26
|
+
* Tools are discovered and stored internally. They are NOT registered with mcpi
|
|
27
|
+
* automatically — that responsibility belongs to the access tiers.
|
|
28
|
+
*/
|
|
29
|
+
export declare class McpClientManager {
|
|
30
|
+
private readonly connections;
|
|
31
|
+
private readonly elicitation;
|
|
32
|
+
private readonly clientFactory;
|
|
33
|
+
private readonly transportFactory;
|
|
34
|
+
private skillsExtensionRequested;
|
|
35
|
+
constructor(options?: McpClientManagerOptions);
|
|
36
|
+
/** Whether this manager advertises the draft skills extension. */
|
|
37
|
+
requestsSkillsExtension(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Turn the draft skills extension on or off for future connections.
|
|
40
|
+
*
|
|
41
|
+
* The host reads the gate from config, which loads after this manager is
|
|
42
|
+
* constructed. Already-open connections keep whatever they negotiated at
|
|
43
|
+
* initialize, because the capability set is fixed for a session.
|
|
44
|
+
*/
|
|
45
|
+
enableSkillsExtension(enabled: boolean): void;
|
|
46
|
+
/**
|
|
47
|
+
* Connect to all servers defined in the config.
|
|
48
|
+
* Connections that fail are logged and skipped — partial success is fine.
|
|
49
|
+
*/
|
|
50
|
+
connectAll(config: McpConfig, log?: (msg: string) => void): Promise<void>;
|
|
51
|
+
/** Connect to a single server by name. */
|
|
52
|
+
connectOne(name: string, serverConfig: ServerConfig, log?: (msg: string) => void): Promise<void>;
|
|
53
|
+
/** Call a tool and return only its terminal protocol result. */
|
|
54
|
+
callTool(serverName: string, toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<TerminalCallToolResult>;
|
|
55
|
+
/** List a server's resources. Discovery only; authorization lives in McpPolicy. */
|
|
56
|
+
listResources(serverName: string, signal?: AbortSignal): Promise<Resource[]>;
|
|
57
|
+
/** List a server's resource templates. Authorization lives in McpPolicy. */
|
|
58
|
+
listResourceTemplates(serverName: string, signal?: AbortSignal): Promise<ResourceTemplateType[]>;
|
|
59
|
+
/** Read a single resource. Authorization is the caller-side policy's responsibility. */
|
|
60
|
+
readResource(serverName: string, uri: string, signal?: AbortSignal): Promise<ReadResourceResult>;
|
|
61
|
+
/**
|
|
62
|
+
* The settings a server declared for one extension during `initialize`, or
|
|
63
|
+
* `undefined` if it declared no such extension.
|
|
64
|
+
*/
|
|
65
|
+
getExtensionCapability(serverName: string, extensionName: string): Record<string, unknown> | undefined;
|
|
66
|
+
/** Draft SEP-2640 `skills/list`. */
|
|
67
|
+
requestSkillsList(serverName: string, cursor?: string, signal?: AbortSignal): Promise<SkillsListResult>;
|
|
68
|
+
/** Draft SEP-2640 `skills/get`. */
|
|
69
|
+
requestSkillsGet(serverName: string, uri: string, signal?: AbortSignal): Promise<SkillsGetResult>;
|
|
70
|
+
/** Draft SEP-2640 `resources/directory/read`. */
|
|
71
|
+
requestDirectoryRead(serverName: string, uri: string, cursor?: string, signal?: AbortSignal): Promise<DirectoryReadResult>;
|
|
72
|
+
private requestExtension;
|
|
73
|
+
/** Disconnect a single server. */
|
|
74
|
+
disconnectOne(name: string): Promise<void>;
|
|
75
|
+
/** Disconnect all servers. */
|
|
76
|
+
disconnectAll(): Promise<void>;
|
|
77
|
+
/** Get all discovered tools across all connected servers. */
|
|
78
|
+
getTools(): McpTool[];
|
|
79
|
+
/** Get tools from a specific server. */
|
|
80
|
+
getToolsForServer(name: string): McpTool[];
|
|
81
|
+
/** Get the MCP Client for discovery APIs that are not tool calls. */
|
|
82
|
+
getClient(name: string): Client | undefined;
|
|
83
|
+
getDiagnostics(name: string): McpClientDiagnostics | undefined;
|
|
84
|
+
/** List connected server names in deterministic order. */
|
|
85
|
+
getConnectedServers(): string[];
|
|
86
|
+
}
|
|
87
|
+
/** Build the SDK's safe child environment without leaking this or a parent bridge endpoint. */
|
|
88
|
+
export declare function buildStdioEnvironment(configured: Readonly<Record<string, string>> | undefined): Record<string, string>;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
|
|
2
|
+
import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
3
|
+
import { DirectoryReadResultSchema, SkillsGetResultSchema, SkillsListResultSchema, } from "../skills/sep2640/protocol.js";
|
|
4
|
+
import { describeNegotiation, SKILLS_METHODS } from "../skills/sep2640/spec.js";
|
|
5
|
+
import { adaptTerminalCallToolResult } from "./call-tool-result.js";
|
|
6
|
+
import { createMcpClient, getMcpClientDiagnostics, MCP_CLIENT_POLICY, } from "./client-factory.js";
|
|
7
|
+
import { McpHostElicitationError } from "./host-elicitation.js";
|
|
8
|
+
const unavailableElicitation = {
|
|
9
|
+
elicit: () => Promise.reject(new McpHostElicitationError("The MCP server requested user input, but no host elicitation integration is configured. The request was not approved.")),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Manages connections to multiple MCP servers and aggregates their tools.
|
|
13
|
+
*
|
|
14
|
+
* Tools are discovered and stored internally. They are NOT registered with mcpi
|
|
15
|
+
* automatically — that responsibility belongs to the access tiers.
|
|
16
|
+
*/
|
|
17
|
+
export class McpClientManager {
|
|
18
|
+
connections = new Map();
|
|
19
|
+
elicitation;
|
|
20
|
+
clientFactory;
|
|
21
|
+
transportFactory;
|
|
22
|
+
skillsExtensionRequested;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.elicitation = options.elicitation ?? unavailableElicitation;
|
|
25
|
+
this.clientFactory = options.clientFactory ?? createMcpClient;
|
|
26
|
+
this.transportFactory = options.transportFactory ?? createTransport;
|
|
27
|
+
this.skillsExtensionRequested = options.skillsExtension === true;
|
|
28
|
+
}
|
|
29
|
+
/** Whether this manager advertises the draft skills extension. */
|
|
30
|
+
requestsSkillsExtension() {
|
|
31
|
+
return this.skillsExtensionRequested;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Turn the draft skills extension on or off for future connections.
|
|
35
|
+
*
|
|
36
|
+
* The host reads the gate from config, which loads after this manager is
|
|
37
|
+
* constructed. Already-open connections keep whatever they negotiated at
|
|
38
|
+
* initialize, because the capability set is fixed for a session.
|
|
39
|
+
*/
|
|
40
|
+
enableSkillsExtension(enabled) {
|
|
41
|
+
this.skillsExtensionRequested = enabled;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Connect to all servers defined in the config.
|
|
45
|
+
* Connections that fail are logged and skipped — partial success is fine.
|
|
46
|
+
*/
|
|
47
|
+
async connectAll(config, log = console.error) {
|
|
48
|
+
const entries = Object.entries(config.mcpServers).sort(([left], [right]) => compareStrings(left, right));
|
|
49
|
+
const results = await Promise.allSettled(entries.map(([name, serverConfig]) => this.connectOne(name, serverConfig, log)));
|
|
50
|
+
for (let index = 0; index < results.length; index++) {
|
|
51
|
+
const result = results[index];
|
|
52
|
+
if (result.status === "rejected") {
|
|
53
|
+
log(`[mcp] Failed to connect to "${entries[index][0]}": ${formatError(result.reason)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Connect to a single server by name. */
|
|
58
|
+
async connectOne(name, serverConfig, log = console.error) {
|
|
59
|
+
if (this.connections.has(name)) {
|
|
60
|
+
await this.disconnectOne(name);
|
|
61
|
+
}
|
|
62
|
+
const transport = this.transportFactory(serverConfig);
|
|
63
|
+
const client = this.clientFactory({
|
|
64
|
+
elicitation: this.elicitation,
|
|
65
|
+
skillsExtension: this.skillsExtensionRequested,
|
|
66
|
+
listChanged: {
|
|
67
|
+
tools: {
|
|
68
|
+
onChanged: (error, tools) => {
|
|
69
|
+
if (error) {
|
|
70
|
+
log(`[mcp] Failed to refresh tools for "${name}": ${formatError(error)}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (tools === null)
|
|
74
|
+
return;
|
|
75
|
+
const connection = this.connections.get(name);
|
|
76
|
+
if (connection?.client === client) {
|
|
77
|
+
connection.tools = toMcpTools(name, tools);
|
|
78
|
+
log(`[mcp] Tools updated for "${name}" (${connection.tools.length} tools)`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
this.connections.set(name, { client, transport, tools: [] });
|
|
85
|
+
try {
|
|
86
|
+
await client.connect(transport, {
|
|
87
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
88
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
89
|
+
});
|
|
90
|
+
const toolsResult = await client.listTools(undefined, {
|
|
91
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
92
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
93
|
+
});
|
|
94
|
+
const tools = toMcpTools(name, toolsResult.tools);
|
|
95
|
+
const diagnostics = getMcpClientDiagnostics(client, {
|
|
96
|
+
skillsExtensionRequested: this.skillsExtensionRequested,
|
|
97
|
+
});
|
|
98
|
+
const connection = this.connections.get(name);
|
|
99
|
+
if (connection?.client === client) {
|
|
100
|
+
connection.tools = tools;
|
|
101
|
+
connection.diagnostics = diagnostics;
|
|
102
|
+
}
|
|
103
|
+
log(`[mcp] Connected to "${name}" (${tools.length} tools, protocol era: ${diagnostics.protocolEra ?? "unknown"})`);
|
|
104
|
+
if (this.skillsExtensionRequested) {
|
|
105
|
+
log(`[mcp] "${name}": ${describeNegotiation(name, diagnostics.skillsExtension.serverCapability)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const connection = this.connections.get(name);
|
|
110
|
+
if (connection?.client === client) {
|
|
111
|
+
this.connections.delete(name);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await client.close();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Preserve the original connection failure.
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Call a tool and return only its terminal protocol result. */
|
|
123
|
+
async callTool(serverName, toolName, args, signal) {
|
|
124
|
+
const connection = this.connections.get(serverName);
|
|
125
|
+
if (!connection) {
|
|
126
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
127
|
+
}
|
|
128
|
+
const result = await connection.client.callTool({ name: toolName, arguments: args }, {
|
|
129
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
130
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
131
|
+
...(signal !== undefined ? { signal } : {}),
|
|
132
|
+
});
|
|
133
|
+
return adaptTerminalCallToolResult(result);
|
|
134
|
+
}
|
|
135
|
+
/** List a server's resources. Discovery only; authorization lives in McpPolicy. */
|
|
136
|
+
async listResources(serverName, signal) {
|
|
137
|
+
const connection = this.connections.get(serverName);
|
|
138
|
+
if (!connection) {
|
|
139
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
140
|
+
}
|
|
141
|
+
const result = await connection.client.listResources(undefined, {
|
|
142
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
143
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
144
|
+
...(signal !== undefined ? { signal } : {}),
|
|
145
|
+
});
|
|
146
|
+
return [...result.resources].sort(compareResources);
|
|
147
|
+
}
|
|
148
|
+
/** List a server's resource templates. Authorization lives in McpPolicy. */
|
|
149
|
+
async listResourceTemplates(serverName, signal) {
|
|
150
|
+
const connection = this.connections.get(serverName);
|
|
151
|
+
if (!connection) {
|
|
152
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
153
|
+
}
|
|
154
|
+
const result = await connection.client.listResourceTemplates(undefined, {
|
|
155
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
156
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
157
|
+
...(signal !== undefined ? { signal } : {}),
|
|
158
|
+
});
|
|
159
|
+
return [...result.resourceTemplates].sort(compareResourceTemplates);
|
|
160
|
+
}
|
|
161
|
+
/** Read a single resource. Authorization is the caller-side policy's responsibility. */
|
|
162
|
+
async readResource(serverName, uri, signal) {
|
|
163
|
+
const connection = this.connections.get(serverName);
|
|
164
|
+
if (!connection) {
|
|
165
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
166
|
+
}
|
|
167
|
+
return connection.client.readResource({ uri }, {
|
|
168
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
169
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
170
|
+
...(signal !== undefined ? { signal } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Draft SEP-2640 skills extension transport
|
|
175
|
+
//
|
|
176
|
+
// These are transport methods, not authorization. `McpPolicy` decides whether
|
|
177
|
+
// a call may happen; this class only knows how to put it on the wire and how
|
|
178
|
+
// to refuse to hand back a payload that does not match the draft schema.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* The settings a server declared for one extension during `initialize`, or
|
|
182
|
+
* `undefined` if it declared no such extension.
|
|
183
|
+
*/
|
|
184
|
+
getExtensionCapability(serverName, extensionName) {
|
|
185
|
+
const capability = this.connections.get(serverName)?.client.getServerCapabilities()
|
|
186
|
+
?.extensions?.[extensionName];
|
|
187
|
+
if (capability === undefined) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (capability === null || typeof capability !== "object" || Array.isArray(capability)) {
|
|
191
|
+
// An extension declared with a non-object body is still "declared"; treat
|
|
192
|
+
// it as declared-with-no-settings rather than inventing settings for it.
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
return capability;
|
|
196
|
+
}
|
|
197
|
+
/** Draft SEP-2640 `skills/list`. */
|
|
198
|
+
async requestSkillsList(serverName, cursor, signal) {
|
|
199
|
+
return this.requestExtension(serverName, SKILLS_METHODS.list, cursor !== undefined ? { cursor } : {}, SkillsListResultSchema, signal);
|
|
200
|
+
}
|
|
201
|
+
/** Draft SEP-2640 `skills/get`. */
|
|
202
|
+
async requestSkillsGet(serverName, uri, signal) {
|
|
203
|
+
return this.requestExtension(serverName, SKILLS_METHODS.get, { uri }, SkillsGetResultSchema, signal);
|
|
204
|
+
}
|
|
205
|
+
/** Draft SEP-2640 `resources/directory/read`. */
|
|
206
|
+
async requestDirectoryRead(serverName, uri, cursor, signal) {
|
|
207
|
+
return this.requestExtension(serverName, SKILLS_METHODS.directoryRead, cursor !== undefined ? { uri, cursor } : { uri }, DirectoryReadResultSchema, signal);
|
|
208
|
+
}
|
|
209
|
+
async requestExtension(serverName, method, params, schema, signal) {
|
|
210
|
+
const connection = this.connections.get(serverName);
|
|
211
|
+
if (!connection) {
|
|
212
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
213
|
+
}
|
|
214
|
+
return connection.client.request({ method, params }, schema, {
|
|
215
|
+
timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
|
|
216
|
+
maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
|
|
217
|
+
...(signal !== undefined ? { signal } : {}),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Disconnect a single server. */
|
|
221
|
+
async disconnectOne(name) {
|
|
222
|
+
const connection = this.connections.get(name);
|
|
223
|
+
if (!connection)
|
|
224
|
+
return;
|
|
225
|
+
this.connections.delete(name);
|
|
226
|
+
try {
|
|
227
|
+
await connection.client.close();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// The connection is already removed; shutdown is best-effort.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Disconnect all servers. */
|
|
234
|
+
async disconnectAll() {
|
|
235
|
+
const names = this.getConnectedServers();
|
|
236
|
+
await Promise.allSettled(names.map((name) => this.disconnectOne(name)));
|
|
237
|
+
}
|
|
238
|
+
/** Get all discovered tools across all connected servers. */
|
|
239
|
+
getTools() {
|
|
240
|
+
return [...this.connections.values()]
|
|
241
|
+
.flatMap((connection) => connection.tools)
|
|
242
|
+
.sort(compareTools);
|
|
243
|
+
}
|
|
244
|
+
/** Get tools from a specific server. */
|
|
245
|
+
getToolsForServer(name) {
|
|
246
|
+
return [...(this.connections.get(name)?.tools ?? [])].sort(compareTools);
|
|
247
|
+
}
|
|
248
|
+
/** Get the MCP Client for discovery APIs that are not tool calls. */
|
|
249
|
+
getClient(name) {
|
|
250
|
+
return this.connections.get(name)?.client;
|
|
251
|
+
}
|
|
252
|
+
getDiagnostics(name) {
|
|
253
|
+
return this.connections.get(name)?.diagnostics;
|
|
254
|
+
}
|
|
255
|
+
/** List connected server names in deterministic order. */
|
|
256
|
+
getConnectedServers() {
|
|
257
|
+
return [...this.connections.keys()].sort(compareStrings);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function createTransport(config) {
|
|
261
|
+
if (config.type === "stdio") {
|
|
262
|
+
return new StdioClientTransport({
|
|
263
|
+
command: config.command,
|
|
264
|
+
args: config.args,
|
|
265
|
+
env: buildStdioEnvironment(config.env),
|
|
266
|
+
cwd: config.cwd,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
270
|
+
requestInit: {
|
|
271
|
+
headers: config.headers ?? {},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Build the SDK's safe child environment without leaking this or a parent bridge endpoint. */
|
|
276
|
+
export function buildStdioEnvironment(configured) {
|
|
277
|
+
const environment = { ...getDefaultEnvironment(), ...configured };
|
|
278
|
+
for (const name of Object.keys(environment)) {
|
|
279
|
+
if (name.startsWith("TOOL_CLI_")) {
|
|
280
|
+
Reflect.deleteProperty(environment, name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return environment;
|
|
284
|
+
}
|
|
285
|
+
function toMcpTools(serverName, tools) {
|
|
286
|
+
return tools
|
|
287
|
+
.map((tool) => ({
|
|
288
|
+
...tool,
|
|
289
|
+
serverName,
|
|
290
|
+
}))
|
|
291
|
+
.sort(compareTools);
|
|
292
|
+
}
|
|
293
|
+
function compareTools(left, right) {
|
|
294
|
+
return compareStrings(left.serverName, right.serverName) || compareStrings(left.name, right.name);
|
|
295
|
+
}
|
|
296
|
+
function compareResources(left, right) {
|
|
297
|
+
return compareStrings(left.uri, right.uri) || compareStrings(left.name, right.name);
|
|
298
|
+
}
|
|
299
|
+
function compareResourceTemplates(left, right) {
|
|
300
|
+
return (compareStrings(left.uriTemplate, right.uriTemplate) || compareStrings(left.name, right.name));
|
|
301
|
+
}
|
|
302
|
+
function compareStrings(left, right) {
|
|
303
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
304
|
+
}
|
|
305
|
+
function formatError(error) {
|
|
306
|
+
return error instanceof Error ? error.message : String(error);
|
|
307
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Load and validate an MCP config file.
|
|
4
|
+
* Returns an empty config if the file doesn't exist.
|
|
5
|
+
* Throws on invalid JSON or schema violations.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadMcpConfig(configPath?: string): Promise<McpConfig>;
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
const DEFAULT_CONFIG_PATH = join(homedir(), ".config", "mcpi-ext", "mcp.json");
|
|
7
|
+
/**
|
|
8
|
+
* Load and validate an MCP config file.
|
|
9
|
+
* Returns an empty config if the file doesn't exist.
|
|
10
|
+
* Throws on invalid JSON or schema violations.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadMcpConfig(configPath) {
|
|
13
|
+
const path = configPath ?? DEFAULT_CONFIG_PATH;
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(path, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err.code === "ENOENT") {
|
|
20
|
+
return { mcpServers: {} };
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Failed to read MCP config at ${path}: ${err.message}`, {
|
|
23
|
+
cause: err,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error(`Invalid JSON in MCP config at ${path}`);
|
|
32
|
+
}
|
|
33
|
+
if (!Value.Check(McpConfig, parsed)) {
|
|
34
|
+
const errors = Value.Errors(McpConfig, parsed);
|
|
35
|
+
const details = errors
|
|
36
|
+
.slice(0, 5)
|
|
37
|
+
.map((e) => ` ${e.instancePath || "/"}: ${e.message}`)
|
|
38
|
+
.join("\n");
|
|
39
|
+
throw new Error(`Invalid MCP config at ${path}:\n${details}`);
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
/** Configuration for a stdio-based MCP server (spawns a child process). */
|
|
3
|
+
export declare const StdioServerConfig: Type.TObject<{
|
|
4
|
+
type: Type.TLiteral<"stdio">;
|
|
5
|
+
command: Type.TString;
|
|
6
|
+
args: Type.TOptional<Type.TArray<Type.TString>>;
|
|
7
|
+
env: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
8
|
+
cwd: Type.TOptional<Type.TString>;
|
|
9
|
+
}>;
|
|
10
|
+
/** Configuration for a remote MCP server (Streamable HTTP). */
|
|
11
|
+
export declare const RemoteServerConfig: Type.TObject<{
|
|
12
|
+
type: Type.TLiteral<"remote">;
|
|
13
|
+
url: Type.TString;
|
|
14
|
+
headers: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
15
|
+
}>;
|
|
16
|
+
/** A single MCP server entry — either stdio or remote. */
|
|
17
|
+
export declare const ServerConfig: Type.TUnion<[Type.TObject<{
|
|
18
|
+
type: Type.TLiteral<"stdio">;
|
|
19
|
+
command: Type.TString;
|
|
20
|
+
args: Type.TOptional<Type.TArray<Type.TString>>;
|
|
21
|
+
env: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
22
|
+
cwd: Type.TOptional<Type.TString>;
|
|
23
|
+
}>, Type.TObject<{
|
|
24
|
+
type: Type.TLiteral<"remote">;
|
|
25
|
+
url: Type.TString;
|
|
26
|
+
headers: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
27
|
+
}>]>;
|
|
28
|
+
/** Top-level configuration file schema. */
|
|
29
|
+
export declare const McpConfig: Type.TObject<{
|
|
30
|
+
mcpServers: Type.TRecord<"^.*$", Type.TUnion<[Type.TObject<{
|
|
31
|
+
type: Type.TLiteral<"stdio">;
|
|
32
|
+
command: Type.TString;
|
|
33
|
+
args: Type.TOptional<Type.TArray<Type.TString>>;
|
|
34
|
+
env: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
35
|
+
cwd: Type.TOptional<Type.TString>;
|
|
36
|
+
}>, Type.TObject<{
|
|
37
|
+
type: Type.TLiteral<"remote">;
|
|
38
|
+
url: Type.TString;
|
|
39
|
+
headers: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
40
|
+
}>]>>;
|
|
41
|
+
experimental: Type.TOptional<Type.TObject<{
|
|
42
|
+
skillsExtension: Type.TOptional<Type.TBoolean>;
|
|
43
|
+
}>>;
|
|
44
|
+
}>;
|
|
45
|
+
export type StdioServerConfig = Static<typeof StdioServerConfig>;
|
|
46
|
+
export type RemoteServerConfig = Static<typeof RemoteServerConfig>;
|
|
47
|
+
export type ServerConfig = Static<typeof ServerConfig>;
|
|
48
|
+
export type McpConfig = Static<typeof McpConfig>;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the draft skills extension is enabled.
|
|
51
|
+
*
|
|
52
|
+
* Defaults to **off**. SEP-2640 is a draft: nothing here should start speaking
|
|
53
|
+
* it because a config file happened to omit a key, and a host that quietly
|
|
54
|
+
* enabled a proposal would make it indistinguishable from ratified support.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isSkillsExtensionEnabled(config: Pick<McpConfig, "experimental">): boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
/** Configuration for a stdio-based MCP server (spawns a child process). */
|
|
3
|
+
export const StdioServerConfig = Type.Object({
|
|
4
|
+
type: Type.Literal("stdio"),
|
|
5
|
+
command: Type.String({ description: "Executable to run" }),
|
|
6
|
+
args: Type.Optional(Type.Array(Type.String(), { description: "Command-line arguments" })),
|
|
7
|
+
env: Type.Optional(Type.Record(Type.String(), Type.String(), {
|
|
8
|
+
description: "Extra environment variables for the child process",
|
|
9
|
+
})),
|
|
10
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the child process" })),
|
|
11
|
+
});
|
|
12
|
+
/** Configuration for a remote MCP server (Streamable HTTP). */
|
|
13
|
+
export const RemoteServerConfig = Type.Object({
|
|
14
|
+
type: Type.Literal("remote"),
|
|
15
|
+
url: Type.String({ description: "HTTP(S) endpoint URL" }),
|
|
16
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String(), {
|
|
17
|
+
description: "Extra HTTP headers (e.g. Authorization)",
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
/** A single MCP server entry — either stdio or remote. */
|
|
21
|
+
export const ServerConfig = Type.Union([StdioServerConfig, RemoteServerConfig]);
|
|
22
|
+
/** Top-level configuration file schema. */
|
|
23
|
+
export const McpConfig = Type.Object({
|
|
24
|
+
mcpServers: Type.Record(Type.String(), ServerConfig, {
|
|
25
|
+
description: "Named MCP server configurations",
|
|
26
|
+
}),
|
|
27
|
+
experimental: Type.Optional(Type.Object({
|
|
28
|
+
skillsExtension: Type.Optional(Type.Boolean({
|
|
29
|
+
description: "Opt in to the DRAFT MCP skills extension (SEP-2640). Unratified and subject to change; off by default.",
|
|
30
|
+
})),
|
|
31
|
+
}, { description: "Opt-in support for unratified MCP proposals" })),
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Whether the draft skills extension is enabled.
|
|
35
|
+
*
|
|
36
|
+
* Defaults to **off**. SEP-2640 is a draft: nothing here should start speaking
|
|
37
|
+
* it because a config file happened to omit a key, and a host that quietly
|
|
38
|
+
* enabled a proposal would make it indistinguishable from ratified support.
|
|
39
|
+
*/
|
|
40
|
+
export function isSkillsExtensionEnabled(config) {
|
|
41
|
+
return config.experimental?.skillsExtension === true;
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpPolicyGateway } from "./policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Gateway behaviour for a server that does not speak the draft SEP-2640 skills
|
|
4
|
+
* extension.
|
|
5
|
+
*
|
|
6
|
+
* `getExtensionCapability` returns `undefined` — the honest answer for a server
|
|
7
|
+
* that declared nothing — and the three request methods reject. They reject
|
|
8
|
+
* rather than return empty results because reaching them at all would mean the
|
|
9
|
+
* policy dispatched an extension request to a server that never declared the
|
|
10
|
+
* extension, and a silent empty result would hide that bug.
|
|
11
|
+
*
|
|
12
|
+
* Spread this into any gateway that only implements the classic MCP surface.
|
|
13
|
+
*/
|
|
14
|
+
export declare const noSkillsExtensionGateway: Pick<McpPolicyGateway, "getExtensionCapability" | "requestSkillsList" | "requestSkillsGet" | "requestDirectoryRead">;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway behaviour for a server that does not speak the draft SEP-2640 skills
|
|
3
|
+
* extension.
|
|
4
|
+
*
|
|
5
|
+
* `getExtensionCapability` returns `undefined` — the honest answer for a server
|
|
6
|
+
* that declared nothing — and the three request methods reject. They reject
|
|
7
|
+
* rather than return empty results because reaching them at all would mean the
|
|
8
|
+
* policy dispatched an extension request to a server that never declared the
|
|
9
|
+
* extension, and a silent empty result would hide that bug.
|
|
10
|
+
*
|
|
11
|
+
* Spread this into any gateway that only implements the classic MCP surface.
|
|
12
|
+
*/
|
|
13
|
+
export const noSkillsExtensionGateway = {
|
|
14
|
+
getExtensionCapability: () => undefined,
|
|
15
|
+
requestSkillsList: () => Promise.reject(new Error(unsupported("skills/list"))),
|
|
16
|
+
requestSkillsGet: () => Promise.reject(new Error(unsupported("skills/get"))),
|
|
17
|
+
requestDirectoryRead: () => Promise.reject(new Error(unsupported("resources/directory/read"))),
|
|
18
|
+
};
|
|
19
|
+
function unsupported(method) {
|
|
20
|
+
return `This MCP server does not declare the skills extension, so ${method} is unavailable.`;
|
|
21
|
+
}
|