@sammorrowdrums/mcpi-ext 0.2.1 → 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 +29 -4
- package/dist/code-mode/eligibility.js +56 -6
- package/dist/code-mode/executor.d.ts +19 -1
- package/dist/code-mode/executor.js +73 -8
- package/dist/code-mode/index.d.ts +54 -12
- package/dist/code-mode/index.js +157 -65
- 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 +4 -1
- package/dist/code-mode/tools.js +7 -4
- package/dist/code-mode/type-hints.d.ts +2 -2
- package/dist/code-mode/type-hints.js +24 -8
- package/dist/docker-e2e.d.ts +0 -1
- package/dist/docker-e2e.js +0 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +146 -67
- 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 +61 -19
- package/dist/mcp/client-manager.js +223 -64
- package/dist/mcp/config-loader.d.ts +0 -1
- package/dist/mcp/config-loader.js +0 -1
- package/dist/mcp/config.d.ts +11 -1
- package/dist/mcp/config.js +15 -1
- 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 +4 -1
- package/dist/mcp/index.js +4 -1
- 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 +5 -3
- package/dist/skills/discover.js +37 -12
- package/dist/skills/format.d.ts +4 -1
- package/dist/skills/format.js +8 -4
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +1 -1
- package/dist/skills/load-skill-tool.d.ts +23 -7
- package/dist/skills/load-skill-tool.js +112 -33
- package/dist/skills/mcp-tool-proxy.d.ts +4 -8
- package/dist/skills/mcp-tool-proxy.js +22 -97
- 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 +48 -2
- package/dist/skills/skill-registry.js +40 -3
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +17 -5
- package/dist/tool-cli/format.js +36 -20
- package/dist/tool-cli/index.d.ts +5 -2
- package/dist/tool-cli/index.js +3 -1
- 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 -30
- package/dist/code-mode/code-mode-integration.test.d.ts +0 -2
- package/dist/code-mode/code-mode-integration.test.d.ts.map +0 -1
- package/dist/code-mode/code-mode-integration.test.js +0 -116
- package/dist/code-mode/code-mode-integration.test.js.map +0 -1
- package/dist/code-mode/eligibility.d.ts.map +0 -1
- package/dist/code-mode/eligibility.js.map +0 -1
- package/dist/code-mode/eligibility.test.d.ts +0 -2
- package/dist/code-mode/eligibility.test.d.ts.map +0 -1
- package/dist/code-mode/eligibility.test.js +0 -53
- package/dist/code-mode/eligibility.test.js.map +0 -1
- package/dist/code-mode/executor.d.ts.map +0 -1
- package/dist/code-mode/executor.js.map +0 -1
- package/dist/code-mode/executor.test.d.ts +0 -2
- package/dist/code-mode/executor.test.d.ts.map +0 -1
- package/dist/code-mode/executor.test.js +0 -130
- package/dist/code-mode/executor.test.js.map +0 -1
- package/dist/code-mode/index.d.ts.map +0 -1
- package/dist/code-mode/index.js.map +0 -1
- package/dist/code-mode/tools.d.ts.map +0 -1
- package/dist/code-mode/tools.js.map +0 -1
- package/dist/code-mode/type-hints.d.ts.map +0 -1
- package/dist/code-mode/type-hints.js.map +0 -1
- package/dist/code-mode/type-hints.test.d.ts +0 -2
- package/dist/code-mode/type-hints.test.d.ts.map +0 -1
- package/dist/code-mode/type-hints.test.js +0 -157
- package/dist/code-mode/type-hints.test.js.map +0 -1
- package/dist/docker-e2e.d.ts.map +0 -1
- package/dist/docker-e2e.js.map +0 -1
- package/dist/docker-e2e.test.d.ts +0 -2
- package/dist/docker-e2e.test.d.ts.map +0 -1
- package/dist/docker-e2e.test.js +0 -176
- package/dist/docker-e2e.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/mcp/client-manager.d.ts.map +0 -1
- package/dist/mcp/client-manager.js.map +0 -1
- package/dist/mcp/client-manager.test.d.ts +0 -2
- package/dist/mcp/client-manager.test.d.ts.map +0 -1
- package/dist/mcp/client-manager.test.js +0 -204
- package/dist/mcp/client-manager.test.js.map +0 -1
- package/dist/mcp/config-loader.d.ts.map +0 -1
- package/dist/mcp/config-loader.js.map +0 -1
- package/dist/mcp/config-loader.test.d.ts +0 -2
- package/dist/mcp/config-loader.test.d.ts.map +0 -1
- package/dist/mcp/config-loader.test.js +0 -93
- package/dist/mcp/config-loader.test.js.map +0 -1
- package/dist/mcp/config.d.ts.map +0 -1
- package/dist/mcp/config.js.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js.map +0 -1
- package/dist/skills/discover.d.ts.map +0 -1
- package/dist/skills/discover.js.map +0 -1
- package/dist/skills/format.d.ts.map +0 -1
- package/dist/skills/format.js.map +0 -1
- package/dist/skills/format.test.d.ts +0 -2
- package/dist/skills/format.test.d.ts.map +0 -1
- package/dist/skills/format.test.js +0 -63
- package/dist/skills/format.test.js.map +0 -1
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js.map +0 -1
- package/dist/skills/load-skill-tool.d.ts.map +0 -1
- package/dist/skills/load-skill-tool.js.map +0 -1
- package/dist/skills/mcp-tool-proxy.d.ts.map +0 -1
- package/dist/skills/mcp-tool-proxy.js.map +0 -1
- package/dist/skills/skill-integration.test.d.ts +0 -2
- package/dist/skills/skill-integration.test.d.ts.map +0 -1
- package/dist/skills/skill-integration.test.js +0 -119
- package/dist/skills/skill-integration.test.js.map +0 -1
- package/dist/skills/skill-registry.d.ts.map +0 -1
- package/dist/skills/skill-registry.js.map +0 -1
- package/dist/skills/skill-registry.test.d.ts +0 -2
- package/dist/skills/skill-registry.test.d.ts.map +0 -1
- package/dist/skills/skill-registry.test.js +0 -67
- package/dist/skills/skill-registry.test.js.map +0 -1
- package/dist/test-servers/weather-server.d.ts +0 -12
- package/dist/test-servers/weather-server.d.ts.map +0 -1
- package/dist/test-servers/weather-server.js +0 -139
- package/dist/test-servers/weather-server.js.map +0 -1
- package/dist/test-servers/weather-stdio.d.ts +0 -2
- package/dist/test-servers/weather-stdio.d.ts.map +0 -1
- package/dist/test-servers/weather-stdio.js +0 -11
- package/dist/test-servers/weather-stdio.js.map +0 -1
- package/dist/tool-cli/cli.d.ts +0 -3
- package/dist/tool-cli/cli.d.ts.map +0 -1
- package/dist/tool-cli/cli.js +0 -179
- package/dist/tool-cli/cli.js.map +0 -1
- package/dist/tool-cli/constants.d.ts +0 -7
- package/dist/tool-cli/constants.d.ts.map +0 -1
- package/dist/tool-cli/constants.js +0 -15
- package/dist/tool-cli/constants.js.map +0 -1
- package/dist/tool-cli/format.d.ts.map +0 -1
- package/dist/tool-cli/format.js.map +0 -1
- package/dist/tool-cli/format.test.d.ts +0 -2
- package/dist/tool-cli/format.test.d.ts.map +0 -1
- package/dist/tool-cli/format.test.js +0 -30
- package/dist/tool-cli/format.test.js.map +0 -1
- package/dist/tool-cli/index.d.ts.map +0 -1
- package/dist/tool-cli/index.js.map +0 -1
- package/dist/tool-cli/rpc-client.d.ts +0 -6
- package/dist/tool-cli/rpc-client.d.ts.map +0 -1
- package/dist/tool-cli/rpc-client.js +0 -25
- package/dist/tool-cli/rpc-client.js.map +0 -1
- package/dist/tool-cli/rpc-server.d.ts +0 -48
- package/dist/tool-cli/rpc-server.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.js +0 -220
- package/dist/tool-cli/rpc-server.js.map +0 -1
- package/dist/tool-cli/rpc-server.test.d.ts +0 -2
- package/dist/tool-cli/rpc-server.test.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.test.js +0 -154
- package/dist/tool-cli/rpc-server.test.js.map +0 -1
|
@@ -1,28 +1,48 @@
|
|
|
1
|
-
import { Client } from "@modelcontextprotocol/
|
|
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";
|
|
2
5
|
import type { McpConfig, ServerConfig } from "./config.js";
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
5
|
-
/** Tool name as reported by the server. */
|
|
6
|
-
name: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
/** JSON Schema for the tool's parameters. */
|
|
9
|
-
inputSchema: Record<string, unknown>;
|
|
10
|
-
/** JSON Schema for structured output, if declared. */
|
|
11
|
-
outputSchema?: Record<string, unknown>;
|
|
12
|
-
/** Tool annotations (readOnlyHint, destructiveHint, etc.). */
|
|
13
|
-
annotations?: Record<string, unknown>;
|
|
6
|
+
/** A lossless MCP tool as discovered from a server, tagged with its origin. */
|
|
7
|
+
export type McpTool = Tool & {
|
|
14
8
|
/** Which configured server this tool came from. */
|
|
15
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;
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Manages connections to multiple MCP servers and aggregates their tools.
|
|
19
25
|
*
|
|
20
|
-
* Tools are discovered and stored internally. They are NOT registered with
|
|
21
|
-
* automatically — that responsibility belongs to the access tiers
|
|
22
|
-
* Football, Code Mode).
|
|
26
|
+
* Tools are discovered and stored internally. They are NOT registered with mcpi
|
|
27
|
+
* automatically — that responsibility belongs to the access tiers.
|
|
23
28
|
*/
|
|
24
29
|
export declare class McpClientManager {
|
|
25
|
-
private connections;
|
|
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;
|
|
26
46
|
/**
|
|
27
47
|
* Connect to all servers defined in the config.
|
|
28
48
|
* Connections that fail are logged and skipped — partial success is fine.
|
|
@@ -30,6 +50,26 @@ export declare class McpClientManager {
|
|
|
30
50
|
connectAll(config: McpConfig, log?: (msg: string) => void): Promise<void>;
|
|
31
51
|
/** Connect to a single server by name. */
|
|
32
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;
|
|
33
73
|
/** Disconnect a single server. */
|
|
34
74
|
disconnectOne(name: string): Promise<void>;
|
|
35
75
|
/** Disconnect all servers. */
|
|
@@ -38,9 +78,11 @@ export declare class McpClientManager {
|
|
|
38
78
|
getTools(): McpTool[];
|
|
39
79
|
/** Get tools from a specific server. */
|
|
40
80
|
getToolsForServer(name: string): McpTool[];
|
|
41
|
-
/** Get the MCP Client for
|
|
81
|
+
/** Get the MCP Client for discovery APIs that are not tool calls. */
|
|
42
82
|
getClient(name: string): Client | undefined;
|
|
43
|
-
|
|
83
|
+
getDiagnostics(name: string): McpClientDiagnostics | undefined;
|
|
84
|
+
/** List connected server names in deterministic order. */
|
|
44
85
|
getConnectedServers(): string[];
|
|
45
86
|
}
|
|
46
|
-
|
|
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>;
|
|
@@ -1,148 +1,307 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StdioClientTransport } from "@modelcontextprotocol/
|
|
3
|
-
import {
|
|
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
|
+
};
|
|
4
11
|
/**
|
|
5
12
|
* Manages connections to multiple MCP servers and aggregates their tools.
|
|
6
13
|
*
|
|
7
|
-
* Tools are discovered and stored internally. They are NOT registered with
|
|
8
|
-
* automatically — that responsibility belongs to the access tiers
|
|
9
|
-
* Football, Code Mode).
|
|
14
|
+
* Tools are discovered and stored internally. They are NOT registered with mcpi
|
|
15
|
+
* automatically — that responsibility belongs to the access tiers.
|
|
10
16
|
*/
|
|
11
17
|
export class McpClientManager {
|
|
12
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
|
+
}
|
|
13
43
|
/**
|
|
14
44
|
* Connect to all servers defined in the config.
|
|
15
45
|
* Connections that fail are logged and skipped — partial success is fine.
|
|
16
46
|
*/
|
|
17
47
|
async connectAll(config, log = console.error) {
|
|
18
|
-
const entries = Object.entries(config.mcpServers);
|
|
48
|
+
const entries = Object.entries(config.mcpServers).sort(([left], [right]) => compareStrings(left, right));
|
|
19
49
|
const results = await Promise.allSettled(entries.map(([name, serverConfig]) => this.connectOne(name, serverConfig, log)));
|
|
20
|
-
for (let
|
|
21
|
-
const result = results[
|
|
50
|
+
for (let index = 0; index < results.length; index++) {
|
|
51
|
+
const result = results[index];
|
|
22
52
|
if (result.status === "rejected") {
|
|
23
|
-
log(`[mcp] Failed to connect to "${entries[
|
|
53
|
+
log(`[mcp] Failed to connect to "${entries[index][0]}": ${formatError(result.reason)}`);
|
|
24
54
|
}
|
|
25
55
|
}
|
|
26
56
|
}
|
|
27
57
|
/** Connect to a single server by name. */
|
|
28
58
|
async connectOne(name, serverConfig, log = console.error) {
|
|
29
|
-
// Disconnect existing connection with this name if any
|
|
30
59
|
if (this.connections.has(name)) {
|
|
31
60
|
await this.disconnectOne(name);
|
|
32
61
|
}
|
|
33
|
-
const transport =
|
|
34
|
-
const client =
|
|
35
|
-
|
|
62
|
+
const transport = this.transportFactory(serverConfig);
|
|
63
|
+
const client = this.clientFactory({
|
|
64
|
+
elicitation: this.elicitation,
|
|
65
|
+
skillsExtension: this.skillsExtensionRequested,
|
|
36
66
|
listChanged: {
|
|
37
67
|
tools: {
|
|
38
|
-
onChanged: (
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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)`);
|
|
45
79
|
}
|
|
46
80
|
},
|
|
47
81
|
},
|
|
48
82
|
},
|
|
49
83
|
});
|
|
50
|
-
// Insert placeholder so listChanged notifications during connect aren't dropped
|
|
51
84
|
this.connections.set(name, { client, transport, tools: [] });
|
|
52
85
|
try {
|
|
53
|
-
await client.connect(transport
|
|
54
|
-
|
|
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
|
+
});
|
|
55
94
|
const tools = toMcpTools(name, toolsResult.tools);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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)}`);
|
|
59
106
|
}
|
|
60
|
-
log(`[mcp] Connected to "${name}" (${tools.length} tools)`);
|
|
61
107
|
}
|
|
62
108
|
catch (error) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (conn && conn.client === client) {
|
|
109
|
+
const connection = this.connections.get(name);
|
|
110
|
+
if (connection?.client === client) {
|
|
66
111
|
this.connections.delete(name);
|
|
67
112
|
}
|
|
68
113
|
try {
|
|
69
114
|
await client.close();
|
|
70
115
|
}
|
|
71
116
|
catch {
|
|
72
|
-
//
|
|
117
|
+
// Preserve the original connection failure.
|
|
73
118
|
}
|
|
74
119
|
throw error;
|
|
75
120
|
}
|
|
76
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
|
+
}
|
|
77
220
|
/** Disconnect a single server. */
|
|
78
221
|
async disconnectOne(name) {
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
222
|
+
const connection = this.connections.get(name);
|
|
223
|
+
if (!connection)
|
|
81
224
|
return;
|
|
82
225
|
this.connections.delete(name);
|
|
83
226
|
try {
|
|
84
|
-
await
|
|
227
|
+
await connection.client.close();
|
|
85
228
|
}
|
|
86
229
|
catch {
|
|
87
|
-
// best-effort
|
|
230
|
+
// The connection is already removed; shutdown is best-effort.
|
|
88
231
|
}
|
|
89
232
|
}
|
|
90
233
|
/** Disconnect all servers. */
|
|
91
234
|
async disconnectAll() {
|
|
92
|
-
const names =
|
|
93
|
-
await Promise.allSettled(names.map((
|
|
235
|
+
const names = this.getConnectedServers();
|
|
236
|
+
await Promise.allSettled(names.map((name) => this.disconnectOne(name)));
|
|
94
237
|
}
|
|
95
238
|
/** Get all discovered tools across all connected servers. */
|
|
96
239
|
getTools() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
return tools;
|
|
240
|
+
return [...this.connections.values()]
|
|
241
|
+
.flatMap((connection) => connection.tools)
|
|
242
|
+
.sort(compareTools);
|
|
102
243
|
}
|
|
103
244
|
/** Get tools from a specific server. */
|
|
104
245
|
getToolsForServer(name) {
|
|
105
|
-
return this.connections.get(name)?.tools ?? [];
|
|
246
|
+
return [...(this.connections.get(name)?.tools ?? [])].sort(compareTools);
|
|
106
247
|
}
|
|
107
|
-
/** Get the MCP Client for
|
|
248
|
+
/** Get the MCP Client for discovery APIs that are not tool calls. */
|
|
108
249
|
getClient(name) {
|
|
109
250
|
return this.connections.get(name)?.client;
|
|
110
251
|
}
|
|
111
|
-
|
|
252
|
+
getDiagnostics(name) {
|
|
253
|
+
return this.connections.get(name)?.diagnostics;
|
|
254
|
+
}
|
|
255
|
+
/** List connected server names in deterministic order. */
|
|
112
256
|
getConnectedServers() {
|
|
113
|
-
return [...this.connections.keys()];
|
|
257
|
+
return [...this.connections.keys()].sort(compareStrings);
|
|
114
258
|
}
|
|
115
259
|
}
|
|
116
260
|
function createTransport(config) {
|
|
117
261
|
if (config.type === "stdio") {
|
|
118
|
-
const env = config.env
|
|
119
|
-
? Object.fromEntries(Object.entries({ ...process.env, ...config.env }).filter((entry) => entry[1] !== undefined))
|
|
120
|
-
: undefined;
|
|
121
262
|
return new StdioClientTransport({
|
|
122
263
|
command: config.command,
|
|
123
264
|
args: config.args,
|
|
124
|
-
env,
|
|
265
|
+
env: buildStdioEnvironment(config.env),
|
|
125
266
|
cwd: config.cwd,
|
|
126
267
|
});
|
|
127
268
|
}
|
|
128
|
-
const headers = {
|
|
129
|
-
...(config.headers ?? {}),
|
|
130
|
-
};
|
|
131
269
|
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
132
270
|
requestInit: {
|
|
133
|
-
headers,
|
|
271
|
+
headers: config.headers ?? {},
|
|
134
272
|
},
|
|
135
273
|
});
|
|
136
274
|
}
|
|
137
|
-
|
|
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
|
+
}
|
|
138
285
|
function toMcpTools(serverName, tools) {
|
|
139
|
-
return tools
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
inputSchema: t.inputSchema,
|
|
143
|
-
outputSchema: t.outputSchema,
|
|
144
|
-
annotations: t.annotations,
|
|
286
|
+
return tools
|
|
287
|
+
.map((tool) => ({
|
|
288
|
+
...tool,
|
|
145
289
|
serverName,
|
|
146
|
-
}))
|
|
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);
|
|
147
307
|
}
|
|
148
|
-
//# sourceMappingURL=client-manager.js.map
|
package/dist/mcp/config.d.ts
CHANGED
|
@@ -38,9 +38,19 @@ export declare const McpConfig: Type.TObject<{
|
|
|
38
38
|
url: Type.TString;
|
|
39
39
|
headers: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
40
40
|
}>]>>;
|
|
41
|
+
experimental: Type.TOptional<Type.TObject<{
|
|
42
|
+
skillsExtension: Type.TOptional<Type.TBoolean>;
|
|
43
|
+
}>>;
|
|
41
44
|
}>;
|
|
42
45
|
export type StdioServerConfig = Static<typeof StdioServerConfig>;
|
|
43
46
|
export type RemoteServerConfig = Static<typeof RemoteServerConfig>;
|
|
44
47
|
export type ServerConfig = Static<typeof ServerConfig>;
|
|
45
48
|
export type McpConfig = Static<typeof McpConfig>;
|
|
46
|
-
|
|
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;
|
package/dist/mcp/config.js
CHANGED
|
@@ -24,5 +24,19 @@ export const McpConfig = Type.Object({
|
|
|
24
24
|
mcpServers: Type.Record(Type.String(), ServerConfig, {
|
|
25
25
|
description: "Named MCP server configurations",
|
|
26
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" })),
|
|
27
32
|
});
|
|
28
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
|
+
import type { McpApprovalPrompt, McpApprovalRequest } from "./policy.js";
|
|
3
|
+
type ApprovalContext = Pick<ExtensionContext, "hasUI" | "ui">;
|
|
4
|
+
/**
|
|
5
|
+
* Bridges policy approval requests to mcpi's explicit user-facing confirmation.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors {@link McpiHostElicitation}: without an interactive UI the request is
|
|
8
|
+
* reported as undecided rather than approved, so nothing is ever silently
|
|
9
|
+
* enabled.
|
|
10
|
+
*/
|
|
11
|
+
export declare class McpiHostApproval implements McpApprovalPrompt {
|
|
12
|
+
private context;
|
|
13
|
+
setContext(context: ApprovalContext | undefined): void;
|
|
14
|
+
confirm(request: McpApprovalRequest): Promise<boolean | undefined>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges policy approval requests to mcpi's explicit user-facing confirmation.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors {@link McpiHostElicitation}: without an interactive UI the request is
|
|
5
|
+
* reported as undecided rather than approved, so nothing is ever silently
|
|
6
|
+
* enabled.
|
|
7
|
+
*/
|
|
8
|
+
export class McpiHostApproval {
|
|
9
|
+
context;
|
|
10
|
+
setContext(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
async confirm(request) {
|
|
14
|
+
const context = this.context;
|
|
15
|
+
if (!context?.hasUI)
|
|
16
|
+
return undefined;
|
|
17
|
+
return context.ui.confirm(request.title, request.message, request.signal ? { signal: request.signal } : undefined);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ElicitRequestParams, ElicitResult } from "@modelcontextprotocol/client";
|
|
2
|
+
import type { ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
3
|
+
import type { McpElicitationHandler } from "./client-factory.js";
|
|
4
|
+
type ElicitationContext = Pick<ExtensionContext, "hasUI" | "ui">;
|
|
5
|
+
export declare class McpHostElicitationError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Bridges MCP form elicitation to mcpi's explicit user-facing UI.
|
|
10
|
+
*
|
|
11
|
+
* URL elicitation is intentionally unsupported and is not advertised by the
|
|
12
|
+
* client. Form requests never receive an implicit approval.
|
|
13
|
+
*/
|
|
14
|
+
export declare class McpiHostElicitation implements McpElicitationHandler {
|
|
15
|
+
private context;
|
|
16
|
+
setContext(context: ElicitationContext | undefined): void;
|
|
17
|
+
elicit(params: ElicitRequestParams): Promise<ElicitResult>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|