@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,3 +1,4 @@
|
|
|
1
|
+
import { getCodeModeDiagnostics, toCodeModeTool } from "./eligibility.js";
|
|
1
2
|
/**
|
|
2
3
|
* Convert a JSON Schema to a TypeScript type string.
|
|
3
4
|
*
|
|
@@ -124,13 +125,14 @@ export function sanitizeToolName(name) {
|
|
|
124
125
|
* method signatures the model can use when writing code.
|
|
125
126
|
*/
|
|
126
127
|
export function generateTypeHints(tools) {
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
const codeModeTools = tools.map(normalizeCodeModeTool);
|
|
129
|
+
const diagnostics = getCodeModeDiagnostics(codeModeTools);
|
|
129
130
|
const methods = [];
|
|
130
|
-
for (const
|
|
131
|
+
for (const codeModeTool of codeModeTools) {
|
|
132
|
+
const tool = codeModeTool.tool;
|
|
131
133
|
const safeName = sanitizeToolName(tool.name);
|
|
132
134
|
const inputSchema = tool.inputSchema;
|
|
133
|
-
const outputSchema =
|
|
135
|
+
const outputSchema = codeModeTool.outputSchema;
|
|
134
136
|
const definitions = (inputSchema.$defs ??
|
|
135
137
|
inputSchema.definitions ??
|
|
136
138
|
outputSchema?.$defs ??
|
|
@@ -140,13 +142,17 @@ export function generateTypeHints(tools) {
|
|
|
140
142
|
// Build output type
|
|
141
143
|
const outputType = outputSchema ? jsonSchemaToTypeString(outputSchema, definitions) : "unknown";
|
|
142
144
|
// Build JSDoc
|
|
143
|
-
const jsdoc = buildJsDoc(
|
|
145
|
+
const jsdoc = buildJsDoc(codeModeTool, inputSchema);
|
|
144
146
|
methods.push(`${jsdoc} ${safeName}: (input: ${inputType}) => Promise<${outputType}>;`);
|
|
145
147
|
}
|
|
146
|
-
const toolListType =
|
|
148
|
+
const toolListType = codeModeTools.length > 0
|
|
149
|
+
? codeModeTools.map((entry) => `"${escapeStr(entry.tool.name)}"`).join(" | ")
|
|
150
|
+
: "never";
|
|
147
151
|
return [
|
|
148
152
|
"// Code mode type hints — auto-generated from MCP tool schemas",
|
|
149
153
|
"// Available tools are accessed via the `codemode` namespace",
|
|
154
|
+
`// MCP catalog: ${diagnostics.totalTools} tool(s); ${diagnostics.callableTools} callable, ${diagnostics.refusedTools} dispatch-refused`,
|
|
155
|
+
`// Output schemas: ${diagnostics.declaredOutputSchemas} declared, ${diagnostics.synthesizedOutputSchemas} synthesized, ${diagnostics.unavailableOutputSchemas} unavailable`,
|
|
150
156
|
"",
|
|
151
157
|
`declare const codemode: {`,
|
|
152
158
|
` /** List all available code mode tool names. */`,
|
|
@@ -166,11 +172,19 @@ function generateInputType(_toolSafeName, inputSchema, definitions) {
|
|
|
166
172
|
const typeStr = jsonSchemaToTypeString(inputSchema, definitions);
|
|
167
173
|
return typeStr;
|
|
168
174
|
}
|
|
169
|
-
function buildJsDoc(
|
|
175
|
+
function buildJsDoc(codeModeTool, inputSchema) {
|
|
176
|
+
const tool = codeModeTool.tool;
|
|
170
177
|
const lines = [" /**"];
|
|
171
178
|
if (tool.description) {
|
|
172
179
|
lines.push(` * ${tool.description}`);
|
|
173
180
|
}
|
|
181
|
+
if (codeModeTool.callable) {
|
|
182
|
+
lines.push(" * Code Mode dispatch: callable (explicitly read-only and non-destructive).");
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push(" * Code Mode dispatch: refused. Use a permission-aware non-Code-Mode path.");
|
|
186
|
+
}
|
|
187
|
+
lines.push(` * Output schema provenance: ${codeModeTool.outputSchemaProvenance}.`);
|
|
174
188
|
const properties = inputSchema.properties;
|
|
175
189
|
if (properties) {
|
|
176
190
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
@@ -186,4 +200,6 @@ function buildJsDoc(tool, inputSchema) {
|
|
|
186
200
|
function escapeStr(s) {
|
|
187
201
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ");
|
|
188
202
|
}
|
|
189
|
-
|
|
203
|
+
function normalizeCodeModeTool(tool) {
|
|
204
|
+
return "tool" in tool && "outputSchemaProvenance" in tool ? tool : toCodeModeTool(tool);
|
|
205
|
+
}
|
package/dist/docker-e2e.d.ts
CHANGED
|
@@ -52,4 +52,3 @@ export declare const dockerE2ETool: {
|
|
|
52
52
|
execute(_toolCallId: string, params: DockerE2EInput, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<DockerE2EDetails> | undefined, _ctx: ExtensionContext): Promise<AgentToolResult<DockerE2EDetails>>;
|
|
53
53
|
};
|
|
54
54
|
export {};
|
|
55
|
-
//# sourceMappingURL=docker-e2e.d.ts.map
|
package/dist/docker-e2e.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@sammorrowdrums/mcpi";
|
|
2
|
+
import type { BashState } from "./routing/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Report whether the host currently exposes a shell tool to the agent.
|
|
5
|
+
*
|
|
6
|
+
* `getActiveTools` may be missing on an older host or throw if the runner context
|
|
7
|
+
* is not bound, and neither is evidence that bash is absent — so both collapse
|
|
8
|
+
* to `undiscoverable` rather than a false negative.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectBashState(pi: ExtensionAPI): BashState;
|
|
2
11
|
export default function (pi: ExtensionAPI): void;
|
|
3
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,39 +1,71 @@
|
|
|
1
1
|
import { CodeModeManager } from "./code-mode/index.js";
|
|
2
2
|
import { dockerE2ETool } from "./docker-e2e.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { McpiHostApproval } from "./mcp/host-approval.js";
|
|
4
|
+
import { McpiHostElicitation } from "./mcp/host-elicitation.js";
|
|
5
|
+
import { McpClientManager, McpPolicy, isSkillsExtensionEnabled, loadMcpConfig, } from "./mcp/index.js";
|
|
6
|
+
import { SkillRegistry, SkillsExtensionClient, createLoadSkillTool, discoverSkillsFromServer, discoverSkillsViaExtension, formatMcpSkillsForPrompt, registerMcpToolProxies, skillsExtensionDiagnostic, } from "./skills/index.js";
|
|
7
|
+
import { ToolCliServer, createPolicyToolProvider, formatToolCliForPrompt, startToolCliBridge, withholdToolCliCredentials, } from "./tool-cli/index.js";
|
|
8
|
+
import { buildExecutionFacilities, formatExecutionFacilities, publishExecutionFacilities, } from "./routing/index.js";
|
|
9
|
+
function errorMessage(err) {
|
|
10
|
+
return err instanceof Error ? err.message : String(err);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Report whether the host currently exposes a shell tool to the agent.
|
|
14
|
+
*
|
|
15
|
+
* `getActiveTools` may be missing on an older host or throw if the runner context
|
|
16
|
+
* is not bound, and neither is evidence that bash is absent — so both collapse
|
|
17
|
+
* to `undiscoverable` rather than a false negative.
|
|
18
|
+
*/
|
|
19
|
+
export function detectBashState(pi) {
|
|
20
|
+
try {
|
|
21
|
+
return pi.getActiveTools().includes("bash")
|
|
22
|
+
? { kind: "registered", toolName: "bash" }
|
|
23
|
+
: { kind: "absent" };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return { kind: "undiscoverable", reason: errorMessage(err) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
6
29
|
export default function (pi) {
|
|
7
30
|
pi.registerTool(dockerE2ETool);
|
|
8
31
|
pi.registerFlag("mcp-config", {
|
|
9
32
|
description: "Path to MCP server configuration JSON file",
|
|
10
33
|
type: "string",
|
|
11
34
|
});
|
|
12
|
-
|
|
35
|
+
pi.registerFlag("mcp-skills-extension", {
|
|
36
|
+
description: "Enable the DRAFT MCP skills extension (SEP-2640). Unratified; off by default.",
|
|
37
|
+
type: "boolean",
|
|
38
|
+
});
|
|
39
|
+
const hostElicitation = new McpiHostElicitation();
|
|
40
|
+
const hostApproval = new McpiHostApproval();
|
|
41
|
+
const mcpManager = new McpClientManager({ elicitation: hostElicitation });
|
|
42
|
+
const policy = new McpPolicy({ gateway: mcpManager, approvals: hostApproval });
|
|
13
43
|
const skillRegistry = new SkillRegistry();
|
|
44
|
+
const skillsClient = new SkillsExtensionClient({ policy });
|
|
14
45
|
const codeModeManager = new CodeModeManager();
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
async callTool(server, tool, args) {
|
|
22
|
-
const client = mcpManager.getClient(server);
|
|
23
|
-
if (!client)
|
|
24
|
-
throw new Error(`No client for server "${server}"`);
|
|
25
|
-
const result = await client.callTool({ name: tool, arguments: args });
|
|
26
|
-
return {
|
|
27
|
-
content: result.content,
|
|
28
|
-
isError: result.isError === true ? true : undefined,
|
|
29
|
-
structuredContent: result.structuredContent,
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
46
|
+
const { codeSearch, codeExecute } = codeModeManager.createTools();
|
|
47
|
+
// Bridge the shared policy boundary to the ToolProvider interface. tool-cli
|
|
48
|
+
// sees exactly the policy-visible discovered schema set, and every call it
|
|
49
|
+
// makes is re-authorized by the same dispatcher, so it cannot reach a hidden
|
|
50
|
+
// tool by naming it directly.
|
|
51
|
+
const toolProvider = createPolicyToolProvider(policy, { upstream: mcpManager });
|
|
33
52
|
const rpcServer = new ToolCliServer(toolProvider);
|
|
53
|
+
// Routing state the prompt reports on. tool-cli is only ever advertised after
|
|
54
|
+
// a compatible authenticated v1 handshake and confirmed bash availability.
|
|
55
|
+
let toolCliState = {
|
|
56
|
+
kind: "not_started",
|
|
57
|
+
reason: "the session has not finished starting",
|
|
58
|
+
};
|
|
59
|
+
let skillsExtensionEnabled = false;
|
|
60
|
+
let facilitiesPublished = false;
|
|
34
61
|
// Register the load_skill tool so the model can activate MCP skills
|
|
35
|
-
pi.registerTool(createLoadSkillTool({ registry: skillRegistry,
|
|
62
|
+
pi.registerTool(createLoadSkillTool({ registry: skillRegistry, policy, skillsClient }));
|
|
63
|
+
pi.registerTool(codeSearch);
|
|
64
|
+
pi.registerTool(codeExecute);
|
|
36
65
|
pi.on("session_start", async (_event, ctx) => {
|
|
66
|
+
hostElicitation.setContext(ctx);
|
|
67
|
+
hostApproval.setContext(ctx);
|
|
68
|
+
withholdToolCliCredentials(pi);
|
|
37
69
|
if (ctx.hasUI) {
|
|
38
70
|
ctx.ui.notify("mcpi-ext loaded", "info");
|
|
39
71
|
}
|
|
@@ -49,6 +81,15 @@ export default function (pi) {
|
|
|
49
81
|
try {
|
|
50
82
|
const config = await loadMcpConfig(configPath);
|
|
51
83
|
const serverCount = Object.keys(config.mcpServers).length;
|
|
84
|
+
// Draft extension support is opt-in from either the config file or the
|
|
85
|
+
// CLI flag, and must be decided before any connection is opened because
|
|
86
|
+
// capabilities are fixed at initialize.
|
|
87
|
+
const skillsExtensionEnabledNow = pi.getFlag("mcp-skills-extension") === true || isSkillsExtensionEnabled(config);
|
|
88
|
+
skillsExtensionEnabled = skillsExtensionEnabledNow;
|
|
89
|
+
mcpManager.enableSkillsExtension(skillsExtensionEnabledNow);
|
|
90
|
+
if (skillsExtensionEnabledNow) {
|
|
91
|
+
log(skillsExtensionDiagnostic());
|
|
92
|
+
}
|
|
52
93
|
if (serverCount > 0) {
|
|
53
94
|
await mcpManager.connectAll(config, log);
|
|
54
95
|
const tools = mcpManager.getTools();
|
|
@@ -56,49 +97,61 @@ export default function (pi) {
|
|
|
56
97
|
// Pre-register all MCP tools as deferred Pi tool proxies
|
|
57
98
|
// (in tools array for dispatch but excluded from system prompt)
|
|
58
99
|
const allToolNames = tools.map((t) => t.name);
|
|
59
|
-
registerMcpToolProxies(allToolNames, mcpManager, pi);
|
|
60
|
-
// Discover skills from all connected servers
|
|
100
|
+
registerMcpToolProxies(allToolNames, mcpManager, policy, pi);
|
|
101
|
+
// Discover skills from all connected servers. A server that declares
|
|
102
|
+
// the draft extension is served entirely by it: the legacy skill://
|
|
103
|
+
// resource scan is a compatibility fallback for servers that do not,
|
|
104
|
+
// never a second opinion on one that does. An empty extension listing
|
|
105
|
+
// therefore means "no skills right now", not "try the old way".
|
|
61
106
|
for (const serverName of mcpManager.getConnectedServers()) {
|
|
62
|
-
const
|
|
63
|
-
if (!client)
|
|
64
|
-
continue;
|
|
107
|
+
const viaExtension = skillsExtensionEnabled && skillsClient.supports(serverName);
|
|
65
108
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
if (viaExtension) {
|
|
110
|
+
const result = await discoverSkillsViaExtension(policy, skillsClient, serverName, log);
|
|
111
|
+
skillRegistry.registerAll(result.skills);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const skills = await discoverSkillsFromServer(policy, serverName, log);
|
|
115
|
+
skillRegistry.registerAll(skills);
|
|
116
|
+
}
|
|
68
117
|
}
|
|
69
118
|
catch (err) {
|
|
70
|
-
log(`[skills] Failed to discover skills from "${serverName}": ${err.message}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (skillRegistry.size > 0) {
|
|
74
|
-
for (const skill of skillRegistry.getAll()) {
|
|
75
|
-
for (const t of skill.allowedTools) {
|
|
76
|
-
gatedToolNames.add(t);
|
|
77
|
-
}
|
|
119
|
+
log(`[skills] Failed to discover skills from "${serverName}" via ${viaExtension ? "the draft skills extension" : "skill:// resources"}: ${err.message}`);
|
|
78
120
|
}
|
|
79
|
-
log(`MCP: ${skillRegistry.size} skill(s) discovered, ${gatedToolNames.size} tool(s) deferred`);
|
|
80
121
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const { port, token } = await rpcServer.start(log);
|
|
84
|
-
pi.setEnv("TOOL_CLI_PORT", String(port));
|
|
85
|
-
pi.setEnv("TOOL_CLI_TOKEN", token);
|
|
122
|
+
for (const collision of skillRegistry.getCollisions()) {
|
|
123
|
+
log(`[skills] Name collision on "${collision.name}": "${collision.challenger.serverName}" exposed as "${collision.registeredAs}" (name held by "${collision.incumbent.serverName}")`);
|
|
86
124
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (codeModeManager.isActive) {
|
|
93
|
-
const { codeSearch, codeExecute } = codeModeManager.createTools();
|
|
94
|
-
pi.registerTool(codeSearch);
|
|
95
|
-
pi.registerTool(codeExecute);
|
|
96
|
-
log(`MCP: Code mode active (${codeModeManager.getEligibleTools().length} eligible tool(s))`);
|
|
125
|
+
// Hand the discovered skills to the policy so their tools are gated
|
|
126
|
+
// everywhere, not just on the mcpi dispatch path.
|
|
127
|
+
policy.registerSkills(skillRegistry.getAll());
|
|
128
|
+
if (skillRegistry.size > 0) {
|
|
129
|
+
log(`MCP: ${skillRegistry.size} skill(s) discovered, ${policy.getGatedToolNames().length} tool(s) deferred`);
|
|
97
130
|
}
|
|
131
|
+
toolCliState = await startToolCliBridge({
|
|
132
|
+
bash: detectBashState(pi),
|
|
133
|
+
server: rpcServer,
|
|
134
|
+
environment: pi,
|
|
135
|
+
log,
|
|
136
|
+
});
|
|
98
137
|
}
|
|
138
|
+
else {
|
|
139
|
+
toolCliState = {
|
|
140
|
+
kind: "not_started",
|
|
141
|
+
reason: "no MCP servers are configured, so there is nothing for it to expose",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Code execution remains available even when no MCP servers or callable tools exist.
|
|
145
|
+
codeModeManager.initialize(mcpManager, policy, log);
|
|
146
|
+
// Probe the optional native sandbox backend once, so routing can state
|
|
147
|
+
// plainly whether code mode can run rather than assuming it can.
|
|
148
|
+
await codeModeManager.probeSandbox();
|
|
99
149
|
}
|
|
100
150
|
catch (err) {
|
|
101
|
-
|
|
151
|
+
withholdToolCliCredentials(pi);
|
|
152
|
+
await rpcServer.stop();
|
|
153
|
+
const msg = `MCP config error: ${errorMessage(err)}`;
|
|
154
|
+
toolCliState = { kind: "not_started", reason: msg };
|
|
102
155
|
if (ctx.hasUI) {
|
|
103
156
|
ctx.ui.notify(msg, "warning");
|
|
104
157
|
}
|
|
@@ -107,42 +160,68 @@ export default function (pi) {
|
|
|
107
160
|
}
|
|
108
161
|
}
|
|
109
162
|
});
|
|
110
|
-
// Inject MCP skills, tool-cli
|
|
163
|
+
// Inject execution routing, MCP skills, tool-cli usage docs, and code mode
|
|
164
|
+
// type hints into the system prompt.
|
|
111
165
|
pi.on("before_agent_start", async (event) => {
|
|
112
166
|
let extra = "";
|
|
113
167
|
const skills = skillRegistry.getAll();
|
|
168
|
+
const bashState = detectBashState(pi);
|
|
169
|
+
// Idempotent after the first resolution; covers hosts that reach
|
|
170
|
+
// before_agent_start without a preceding successful session_start.
|
|
171
|
+
const sandbox = await codeModeManager.probeSandbox();
|
|
172
|
+
// The routing section is emitted on every load, including with zero MCP
|
|
173
|
+
// servers — an agent still needs to know that exact computation and the
|
|
174
|
+
// shell are on the table, and why the MCP-backed facilities are not.
|
|
175
|
+
const facilities = buildExecutionFacilities({
|
|
176
|
+
skills: {
|
|
177
|
+
count: skills.length,
|
|
178
|
+
draftExtensionEnabled: skillsExtensionEnabled,
|
|
179
|
+
},
|
|
180
|
+
codeMode: { active: codeModeManager.isActive, reason: sandbox.reason },
|
|
181
|
+
toolCli: toolCliState,
|
|
182
|
+
bash: bashState,
|
|
183
|
+
});
|
|
184
|
+
// Prefer the host's own facility registry when it grows one. The two paths
|
|
185
|
+
// are mutually exclusive, so the section can never be emitted twice.
|
|
186
|
+
if (!facilitiesPublished) {
|
|
187
|
+
facilitiesPublished = publishExecutionFacilities(pi, facilities);
|
|
188
|
+
}
|
|
189
|
+
if (!facilitiesPublished) {
|
|
190
|
+
extra += formatExecutionFacilities(facilities);
|
|
191
|
+
}
|
|
114
192
|
if (skills.length > 0) {
|
|
115
193
|
extra += formatMcpSkillsForPrompt(skills);
|
|
116
194
|
}
|
|
117
|
-
|
|
118
|
-
extra += formatToolCliForPrompt(serverCount);
|
|
195
|
+
extra += formatToolCliForPrompt({ toolCli: toolCliState, bash: bashState });
|
|
119
196
|
if (codeModeManager.isActive) {
|
|
197
|
+
codeModeManager.refresh();
|
|
120
198
|
extra += codeModeManager.formatSystemPromptSection();
|
|
121
199
|
}
|
|
122
200
|
if (extra.length === 0)
|
|
123
201
|
return;
|
|
124
202
|
return { systemPrompt: event.systemPrompt + extra };
|
|
125
203
|
});
|
|
126
|
-
// Block deferred MCP tools until their skill is loaded
|
|
204
|
+
// Block deferred MCP tools until their skill is loaded. The policy owns the
|
|
205
|
+
// decision so mcpi dispatch and the RPC path cannot disagree.
|
|
127
206
|
pi.on("tool_call", async (event) => {
|
|
128
207
|
const name = "toolName" in event ? event.toolName : undefined;
|
|
129
|
-
if (!name || !
|
|
208
|
+
if (!name || !policy.isGated(name))
|
|
130
209
|
return;
|
|
131
|
-
const relevantSkills =
|
|
132
|
-
.getAll()
|
|
133
|
-
.filter((s) => s.allowedTools.includes(name))
|
|
134
|
-
.map((s) => s.name);
|
|
210
|
+
const relevantSkills = policy.getGatingSkills(name);
|
|
135
211
|
return {
|
|
136
212
|
block: true,
|
|
137
213
|
reason: `Tool "${name}" requires loading a skill first. Call load_skill with one of: ${relevantSkills.join(", ")}`,
|
|
138
214
|
};
|
|
139
215
|
});
|
|
140
216
|
pi.on("session_shutdown", async () => {
|
|
141
|
-
|
|
142
|
-
|
|
217
|
+
hostElicitation.setContext(undefined);
|
|
218
|
+
hostApproval.setContext(undefined);
|
|
219
|
+
withholdToolCliCredentials(pi);
|
|
220
|
+
toolCliState = { kind: "not_started", reason: "the session has shut down" };
|
|
221
|
+
facilitiesPublished = false;
|
|
143
222
|
await rpcServer.stop();
|
|
144
223
|
await mcpManager.disconnectAll();
|
|
145
224
|
skillRegistry.clear();
|
|
225
|
+
policy.reset();
|
|
146
226
|
});
|
|
147
227
|
}
|
|
148
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@sammorrowdrums/mcpi";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/client";
|
|
3
|
+
import type { CallToolResult as ToolCliCallToolResult } from "@sammorrowdrums/tool-cli";
|
|
4
|
+
export interface TerminalCallToolResult {
|
|
5
|
+
readonly kind: "terminal";
|
|
6
|
+
readonly result: CallToolResult;
|
|
7
|
+
}
|
|
8
|
+
export declare function adaptTerminalCallToolResult(result: CallToolResult): TerminalCallToolResult;
|
|
9
|
+
export declare function renderTerminalCallToolResult(terminal: TerminalCallToolResult): AgentToolResult<CallToolResult>;
|
|
10
|
+
export declare function toToolCliCallToolResult(terminal: TerminalCallToolResult): ToolCliCallToolResult;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function adaptTerminalCallToolResult(result) {
|
|
2
|
+
return { kind: "terminal", result };
|
|
3
|
+
}
|
|
4
|
+
export function renderTerminalCallToolResult(terminal) {
|
|
5
|
+
const content = [];
|
|
6
|
+
if (terminal.result.isError) {
|
|
7
|
+
content.push({ type: "text", text: "MCP tool reported an error." });
|
|
8
|
+
}
|
|
9
|
+
for (const block of terminal.result.content) {
|
|
10
|
+
switch (block.type) {
|
|
11
|
+
case "text":
|
|
12
|
+
content.push({ type: "text", text: block.text });
|
|
13
|
+
break;
|
|
14
|
+
case "image":
|
|
15
|
+
content.push({ type: "image", data: block.data, mimeType: block.mimeType });
|
|
16
|
+
break;
|
|
17
|
+
case "audio":
|
|
18
|
+
content.push({
|
|
19
|
+
type: "text",
|
|
20
|
+
text: `[MCP audio: ${block.mimeType}; base64 payload preserved in result details]`,
|
|
21
|
+
});
|
|
22
|
+
break;
|
|
23
|
+
case "resource_link":
|
|
24
|
+
content.push({
|
|
25
|
+
type: "text",
|
|
26
|
+
text: formatResourceLink(block),
|
|
27
|
+
});
|
|
28
|
+
break;
|
|
29
|
+
case "resource":
|
|
30
|
+
content.push({
|
|
31
|
+
type: "text",
|
|
32
|
+
text: formatEmbeddedResource(block),
|
|
33
|
+
});
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
content.push({ type: "text", text: JSON.stringify(block, null, 2) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (terminal.result.structuredContent !== undefined) {
|
|
40
|
+
content.push({
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `Structured content:\n${JSON.stringify(terminal.result.structuredContent, null, 2)}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (content.length === 0) {
|
|
46
|
+
content.push({ type: "text", text: terminal.result.isError ? "MCP tool failed." : "" });
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
content,
|
|
50
|
+
details: terminal.result,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function toToolCliCallToolResult(terminal) {
|
|
54
|
+
return {
|
|
55
|
+
...terminal.result,
|
|
56
|
+
content: [...terminal.result.content],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function formatResourceLink(block) {
|
|
60
|
+
const label = block.title ?? block.name ?? block.uri;
|
|
61
|
+
const details = [block.mimeType, block.description].filter(Boolean).join("; ");
|
|
62
|
+
return `[MCP resource link: ${label}] ${block.uri}${details ? ` (${details})` : ""}`;
|
|
63
|
+
}
|
|
64
|
+
function formatEmbeddedResource(block) {
|
|
65
|
+
const resource = block.resource;
|
|
66
|
+
if ("text" in resource) {
|
|
67
|
+
return `[MCP embedded resource: ${resource.uri}]\n${resource.text}`;
|
|
68
|
+
}
|
|
69
|
+
return `[MCP embedded resource: ${resource.uri}; ${resource.mimeType ?? "binary"} blob preserved in result details]`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Client, type DiscoverResult, type ElicitRequestParams, type ElicitResult, type Implementation, type ListChangedHandlers, type ProtocolEra, type ServerCapabilities } from "@modelcontextprotocol/client";
|
|
2
|
+
export declare const MCP_CLIENT_IDENTITY: {
|
|
3
|
+
readonly name: "@sammorrowdrums/mcpi-ext";
|
|
4
|
+
readonly version: "1.0.0";
|
|
5
|
+
};
|
|
6
|
+
export declare const MCP_CLIENT_POLICY: {
|
|
7
|
+
readonly listMaxPages: 64;
|
|
8
|
+
readonly maxInputRequiredRounds: 4;
|
|
9
|
+
readonly probeTimeoutMs: 5000;
|
|
10
|
+
readonly requestTimeoutMs: 60000;
|
|
11
|
+
readonly maxTotalTimeoutMs: number;
|
|
12
|
+
};
|
|
13
|
+
export declare const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly ["2026-07-28", ...string[]];
|
|
14
|
+
export interface McpElicitationHandler {
|
|
15
|
+
elicit(params: ElicitRequestParams): Promise<ElicitResult>;
|
|
16
|
+
}
|
|
17
|
+
export interface McpClientDiagnostics {
|
|
18
|
+
protocolVersion: string | undefined;
|
|
19
|
+
protocolEra: ProtocolEra | undefined;
|
|
20
|
+
discoverResult: DiscoverResult | undefined;
|
|
21
|
+
serverImplementation: Implementation | undefined;
|
|
22
|
+
serverCapabilities: ServerCapabilities | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* What this client requested and what the server declared for the draft
|
|
25
|
+
* skills extension, so a draft feature is never silently in play.
|
|
26
|
+
*/
|
|
27
|
+
skillsExtension: {
|
|
28
|
+
/** Whether this client advertised the extension at initialize. */
|
|
29
|
+
requested: boolean;
|
|
30
|
+
/** Pinned draft revision this implementation targets. */
|
|
31
|
+
revision: string;
|
|
32
|
+
/** Always `"draft"` — this is an unratified proposal. */
|
|
33
|
+
status: string;
|
|
34
|
+
/** Whether the server declared the extension back. */
|
|
35
|
+
serverDeclared: boolean;
|
|
36
|
+
/** Settings the server declared, or `undefined` if it declared none. */
|
|
37
|
+
serverCapability: Record<string, unknown> | undefined;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface CreateMcpClientOptions {
|
|
41
|
+
elicitation: McpElicitationHandler;
|
|
42
|
+
listChanged?: ListChangedHandlers;
|
|
43
|
+
/**
|
|
44
|
+
* Advertise the draft skills extension at initialize.
|
|
45
|
+
*
|
|
46
|
+
* Off by default. When false this client never mentions SEP-2640 on the
|
|
47
|
+
* wire, which is what makes the fallback path a genuine absence of the
|
|
48
|
+
* extension rather than a suppressed one.
|
|
49
|
+
*/
|
|
50
|
+
skillsExtension?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates every MCP client with the same identity, negotiated protocol policy,
|
|
54
|
+
* capabilities, pagination limit, and SDK-managed input_required handling.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createMcpClient(options: CreateMcpClientOptions): Client;
|
|
57
|
+
export declare function getMcpClientDiagnostics(client: Client, options?: {
|
|
58
|
+
skillsExtensionRequested?: boolean;
|
|
59
|
+
}): McpClientDiagnostics;
|
|
@@ -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
|
+
}
|