@frockbot/plugin-mcp 0.1.2 → 0.1.4
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/package.json +8 -8
- package/src/agent.test.ts +12 -13
- package/src/agent.ts +22 -24
- package/src/lifecycle-tools.ts +1 -1
- package/src/records.test.ts +7 -7
- package/src/records.ts +3 -3
- package/src/user.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@frockbot/configuration-core": "0.1.
|
|
29
|
-
"@frockbot/connection-core": "0.1.
|
|
30
|
-
"@frockbot/kernel-contracts": "0.1.
|
|
31
|
-
"@frockbot/plugin-credentials": "0.1.
|
|
32
|
-
"@frockbot/plugin-settings": "0.1.
|
|
33
|
-
"@frockbot/plugin-tools": "0.1.
|
|
34
|
-
"@frockbot/plugin-web": "0.1.
|
|
28
|
+
"@frockbot/configuration-core": "0.1.4",
|
|
29
|
+
"@frockbot/connection-core": "0.1.4",
|
|
30
|
+
"@frockbot/kernel-contracts": "0.1.4",
|
|
31
|
+
"@frockbot/plugin-credentials": "0.1.4",
|
|
32
|
+
"@frockbot/plugin-settings": "0.1.4",
|
|
33
|
+
"@frockbot/plugin-tools": "0.1.4",
|
|
34
|
+
"@frockbot/plugin-web": "0.1.4",
|
|
35
35
|
"cordis": "4.0.0-rc.8"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
package/src/agent.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
createConfiguredMcpRuntimeContribution,
|
|
7
7
|
decodeMcpConnectionSettingsV1,
|
|
8
8
|
MAX_MCP_SERVERS_PER_USER_V1,
|
|
9
|
-
|
|
9
|
+
mcpCapabilityResolutionV1,
|
|
10
10
|
mcpConnectionLifecycleV1,
|
|
11
11
|
mcpServerSlugV1,
|
|
12
12
|
mcpToolNameV1,
|
|
@@ -96,20 +96,19 @@ function server(options: {
|
|
|
96
96
|
async function mount(config: {
|
|
97
97
|
fetch: typeof fetch;
|
|
98
98
|
connection?: ConnectionView;
|
|
99
|
-
|
|
99
|
+
capabilityIndex?: number;
|
|
100
100
|
failures?: string[];
|
|
101
101
|
outcomes?: unknown[];
|
|
102
102
|
}): Promise<{ root: Context; mounted: boolean }> {
|
|
103
103
|
const plugin = await createConfiguredMcpRuntimeContribution({
|
|
104
|
-
|
|
104
|
+
capability: {
|
|
105
105
|
packageId: "mcp",
|
|
106
106
|
capabilityId: "mcp-tools",
|
|
107
107
|
connectionId: "mcp-1",
|
|
108
|
-
state: "enabled",
|
|
109
108
|
},
|
|
110
|
-
...(config.
|
|
109
|
+
...(config.capabilityIndex === undefined
|
|
111
110
|
? {}
|
|
112
|
-
: {
|
|
111
|
+
: { capabilityIndex: config.capabilityIndex }),
|
|
113
112
|
userId: "user-1",
|
|
114
113
|
readSecret: () => undefined,
|
|
115
114
|
authorizeConnection: () =>
|
|
@@ -170,7 +169,7 @@ describe("Connection-scoped settings", () => {
|
|
|
170
169
|
});
|
|
171
170
|
});
|
|
172
171
|
|
|
173
|
-
describe("mounting one
|
|
172
|
+
describe("mounting one enabled Capability", () => {
|
|
174
173
|
test("registers every listed tool with the server's own schema", async () => {
|
|
175
174
|
const { root } = await mount({ fetch: server({}) });
|
|
176
175
|
|
|
@@ -284,7 +283,7 @@ describe("mounting one Assignment", () => {
|
|
|
284
283
|
const { mounted } = await mount({
|
|
285
284
|
failures,
|
|
286
285
|
fetch: server({}),
|
|
287
|
-
|
|
286
|
+
capabilityIndex: MAX_MCP_SERVERS_PER_USER_V1,
|
|
288
287
|
});
|
|
289
288
|
|
|
290
289
|
expect(mounted).toBe(false);
|
|
@@ -385,13 +384,13 @@ describe("the durable lifecycle a mount participates in", () => {
|
|
|
385
384
|
});
|
|
386
385
|
});
|
|
387
386
|
|
|
388
|
-
test("the
|
|
389
|
-
const before =
|
|
390
|
-
|
|
387
|
+
test("the Capability's resolution key moves with the server epoch", () => {
|
|
388
|
+
const before = mcpCapabilityResolutionV1({
|
|
389
|
+
capability: { connectionId: "mcp-1" },
|
|
391
390
|
connection: connection({ safeMetadata: { serverEpoch: 1 } }),
|
|
392
391
|
});
|
|
393
|
-
const restarted =
|
|
394
|
-
|
|
392
|
+
const restarted = mcpCapabilityResolutionV1({
|
|
393
|
+
capability: { connectionId: "mcp-1" },
|
|
395
394
|
connection: connection({ safeMetadata: { serverEpoch: 2 } }),
|
|
396
395
|
});
|
|
397
396
|
expect(restarted).not.toBe(before);
|
package/src/agent.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime Contribution: one enabled
|
|
2
|
+
* The runtime Contribution: one enabled `mcp-tools` Capability becomes one
|
|
3
3
|
* remote MCP server's tools in the Bot's tool registry.
|
|
4
4
|
*
|
|
5
5
|
* Everything the Bot ever sees of a server passes through here, and every byte
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { mcpAuthorizationRequiredV1 } from "./oauth.js";
|
|
28
28
|
import { decodeOutboundMcpUrlV1 } from "./ssrf.js";
|
|
29
29
|
import {
|
|
30
|
-
|
|
30
|
+
mcpCapabilityResolutionKeyV1,
|
|
31
31
|
mcpFailureCodeV1,
|
|
32
32
|
MAX_MCP_INSTRUCTIONS_BYTES_V1,
|
|
33
33
|
type McpFailureCodeV1,
|
|
@@ -62,8 +62,8 @@ export const MCP_TOOL_SUBAGENT_ROLES: readonly string[] = ["executor"];
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* The durable per-User ceiling on remote MCP servers. Counted over the enabled
|
|
65
|
-
*
|
|
66
|
-
* by
|
|
65
|
+
* Capabilities of this Package, so a Bot cannot be handed a seventeenth server
|
|
66
|
+
* by enabling one more Connection.
|
|
67
67
|
*/
|
|
68
68
|
export const MAX_MCP_SERVERS_PER_USER_V1 = 16;
|
|
69
69
|
|
|
@@ -200,18 +200,17 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
export interface McpRuntimeContributionConfig {
|
|
203
|
-
|
|
203
|
+
capability: {
|
|
204
204
|
packageId: string;
|
|
205
205
|
capabilityId: string;
|
|
206
206
|
connectionId?: string;
|
|
207
|
-
state: string;
|
|
208
207
|
};
|
|
209
208
|
/**
|
|
210
|
-
* This
|
|
209
|
+
* This Capability's ordinal among the enabled ones of this Package,
|
|
211
210
|
* which is what makes the per-User server ceiling countable from inside a
|
|
212
|
-
* per-
|
|
211
|
+
* per-Capability factory.
|
|
213
212
|
*/
|
|
214
|
-
|
|
213
|
+
capabilityIndex?: number;
|
|
215
214
|
userId: string;
|
|
216
215
|
readSecret(name: string): string | undefined;
|
|
217
216
|
authorizeConnection(): Promise<ConnectionView>;
|
|
@@ -240,7 +239,7 @@ export interface McpRuntimeContributionConfig {
|
|
|
240
239
|
}
|
|
241
240
|
|
|
242
241
|
/**
|
|
243
|
-
* Resolve one
|
|
242
|
+
* Resolve one enabled Capability into a mounted runtime Plugin. The handshake and
|
|
244
243
|
* `tools/list` happen here, before the Plugin mounts, so a server that cannot
|
|
245
244
|
* be reached contributes nothing instead of half-registering.
|
|
246
245
|
*/
|
|
@@ -248,23 +247,22 @@ export async function createConfiguredMcpRuntimeContribution(
|
|
|
248
247
|
config: McpRuntimeContributionConfig,
|
|
249
248
|
): Promise<Plugin.Function | undefined> {
|
|
250
249
|
if (
|
|
251
|
-
config.
|
|
252
|
-
config.
|
|
253
|
-
config.
|
|
254
|
-
!config.assignment.connectionId
|
|
250
|
+
config.capability.packageId !== MCP_PACKAGE_ID ||
|
|
251
|
+
config.capability.capabilityId !== MCP_CAPABILITY_ID ||
|
|
252
|
+
!config.capability.connectionId
|
|
255
253
|
) {
|
|
256
254
|
return undefined;
|
|
257
255
|
}
|
|
258
|
-
if ((config.
|
|
256
|
+
if ((config.capabilityIndex ?? 0) >= MAX_MCP_SERVERS_PER_USER_V1) {
|
|
259
257
|
config.onFailure?.(
|
|
260
|
-
`A User may
|
|
258
|
+
`A User may enable at most ${MAX_MCP_SERVERS_PER_USER_V1} MCP servers`,
|
|
261
259
|
);
|
|
262
260
|
return undefined;
|
|
263
261
|
}
|
|
264
262
|
const fetchImpl = config.fetch ?? boundFetch;
|
|
265
263
|
let client: McpClient | undefined;
|
|
266
264
|
let lifecycle: { serverEpoch?: number; instructions?: string } = {};
|
|
267
|
-
const connectionId = config.
|
|
265
|
+
const connectionId = config.capability.connectionId;
|
|
268
266
|
try {
|
|
269
267
|
const connection = await config.authorizeConnection();
|
|
270
268
|
lifecycle = mcpConnectionLifecycleV1(connection);
|
|
@@ -275,7 +273,7 @@ export async function createConfiguredMcpRuntimeContribution(
|
|
|
275
273
|
const apiKey =
|
|
276
274
|
connection.connectionTypeId === MCP_KEYED_CONNECTION_TYPE_ID ||
|
|
277
275
|
connection.connectionTypeId === MCP_OAUTH_CONNECTION_TYPE_ID
|
|
278
|
-
? await
|
|
276
|
+
? await openEnabledCredential(config, connection)
|
|
279
277
|
: undefined;
|
|
280
278
|
client = new McpClient({
|
|
281
279
|
url: settings.url,
|
|
@@ -372,18 +370,18 @@ export async function createConfiguredMcpRuntimeContribution(
|
|
|
372
370
|
}
|
|
373
371
|
|
|
374
372
|
/**
|
|
375
|
-
* What one
|
|
373
|
+
* What one enabled `mcp-tools` Capability resolves to, including the server's
|
|
376
374
|
* `serverEpoch`. A restart bumps the epoch, so this key changes, so the next
|
|
377
375
|
* admitted Turn resolves a different mount and re-handshakes — while the
|
|
378
376
|
* in-flight Turn, holding the client it already mounted, is untouched.
|
|
379
377
|
*/
|
|
380
|
-
export function
|
|
381
|
-
|
|
378
|
+
export function mcpCapabilityResolutionV1(config: {
|
|
379
|
+
capability: { connectionId?: string };
|
|
382
380
|
connection: { generation?: string; safeMetadata?: Record<string, unknown> };
|
|
383
381
|
}): string {
|
|
384
382
|
const lifecycle = mcpConnectionLifecycleV1(config.connection);
|
|
385
|
-
return
|
|
386
|
-
connectionId: config.
|
|
383
|
+
return mcpCapabilityResolutionKeyV1({
|
|
384
|
+
connectionId: config.capability.connectionId ?? "",
|
|
387
385
|
...(config.connection.generation === undefined
|
|
388
386
|
? {}
|
|
389
387
|
: { connectionGeneration: config.connection.generation }),
|
|
@@ -393,7 +391,7 @@ export function mcpAssignmentResolutionV1(config: {
|
|
|
393
391
|
});
|
|
394
392
|
}
|
|
395
393
|
|
|
396
|
-
async function
|
|
394
|
+
async function openEnabledCredential(
|
|
397
395
|
config: McpRuntimeContributionConfig,
|
|
398
396
|
connection: ConnectionView,
|
|
399
397
|
): Promise<string> {
|
package/src/lifecycle-tools.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* These tools need no Connection: they read and write the User's own MCP
|
|
14
14
|
* records through the host the Bot's Durable Object supplies, which is why
|
|
15
|
-
* this Contribution mounts for a Turn rather than for
|
|
15
|
+
* this Contribution mounts for a Turn rather than for one Capability.
|
|
16
16
|
*/
|
|
17
17
|
import type {
|
|
18
18
|
Session,
|
package/src/records.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
decodeMcpRefusalRecordV1,
|
|
8
8
|
decodeMcpServerRecordV1,
|
|
9
9
|
decodeMcpServerStatusViewV1,
|
|
10
|
-
|
|
10
|
+
mcpCapabilityResolutionKeyV1,
|
|
11
11
|
mcpConnectionMetadataV1,
|
|
12
12
|
mcpFailureCodeV1,
|
|
13
13
|
type McpServerRecordV1,
|
|
@@ -263,19 +263,19 @@ describe("the mount outcome that crosses back from a Bot", () => {
|
|
|
263
263
|
});
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
describe("the
|
|
266
|
+
describe("the Capability resolution key", () => {
|
|
267
267
|
test("changes when the server epoch changes, and only then", () => {
|
|
268
|
-
const before =
|
|
268
|
+
const before = mcpCapabilityResolutionKeyV1({
|
|
269
269
|
connectionId: "mcp-1",
|
|
270
270
|
connectionGeneration: "gen-1",
|
|
271
271
|
serverEpoch: 1,
|
|
272
272
|
});
|
|
273
|
-
const restarted =
|
|
273
|
+
const restarted = mcpCapabilityResolutionKeyV1({
|
|
274
274
|
connectionId: "mcp-1",
|
|
275
275
|
connectionGeneration: "gen-1",
|
|
276
276
|
serverEpoch: 2,
|
|
277
277
|
});
|
|
278
|
-
const again =
|
|
278
|
+
const again = mcpCapabilityResolutionKeyV1({
|
|
279
279
|
connectionId: "mcp-1",
|
|
280
280
|
connectionGeneration: "gen-1",
|
|
281
281
|
serverEpoch: 1,
|
|
@@ -286,13 +286,13 @@ describe("the Assignment resolution key", () => {
|
|
|
286
286
|
|
|
287
287
|
test("changes when the Connection generation changes", () => {
|
|
288
288
|
expect(
|
|
289
|
-
|
|
289
|
+
mcpCapabilityResolutionKeyV1({
|
|
290
290
|
connectionId: "mcp-1",
|
|
291
291
|
connectionGeneration: "gen-2",
|
|
292
292
|
serverEpoch: 1,
|
|
293
293
|
}),
|
|
294
294
|
).not.toBe(
|
|
295
|
-
|
|
295
|
+
mcpCapabilityResolutionKeyV1({
|
|
296
296
|
connectionId: "mcp-1",
|
|
297
297
|
connectionGeneration: "gen-1",
|
|
298
298
|
serverEpoch: 1,
|
package/src/records.ts
CHANGED
|
@@ -96,7 +96,7 @@ export interface McpServerRecordV1 {
|
|
|
96
96
|
transport: "streamable-http" | "sse";
|
|
97
97
|
instructions?: string;
|
|
98
98
|
/**
|
|
99
|
-
* Bumped by `mcp/restart`. It participates in the
|
|
99
|
+
* Bumped by `mcp/restart`. It participates in the Capability's resolution
|
|
100
100
|
* key, so the next admitted Turn resolves a different mount and
|
|
101
101
|
* re-handshakes; the in-flight Turn keeps the client it already has.
|
|
102
102
|
*/
|
|
@@ -653,12 +653,12 @@ export function decodeMcpMountOutcomeV1(
|
|
|
653
653
|
}
|
|
654
654
|
|
|
655
655
|
/**
|
|
656
|
-
* What one
|
|
656
|
+
* What one enabled `mcp-tools` Capability resolves to. The `serverEpoch` is in it,
|
|
657
657
|
* which is the whole of restart semantics: a restart changes this key, the
|
|
658
658
|
* next admitted Turn resolves a different mount and re-handshakes, and the
|
|
659
659
|
* in-flight Turn — already holding its client — is untouched.
|
|
660
660
|
*/
|
|
661
|
-
export function
|
|
661
|
+
export function mcpCapabilityResolutionKeyV1(input: {
|
|
662
662
|
connectionId: string;
|
|
663
663
|
connectionGeneration?: string;
|
|
664
664
|
serverEpoch?: number;
|
package/src/user.ts
CHANGED
|
@@ -531,7 +531,7 @@ export class McpUserBackendContribution {
|
|
|
531
531
|
if (connection.state === "revoked" || connection.state === "revoking") {
|
|
532
532
|
throw new Error("MCP Connection is revoked");
|
|
533
533
|
}
|
|
534
|
-
// The epoch bump is the whole of restart: it is in the
|
|
534
|
+
// The epoch bump is the whole of restart: it is in the Capability's
|
|
535
535
|
// resolution key, so the next admitted Turn resolves a different
|
|
536
536
|
// mount and re-handshakes, while the in-flight Turn keeps the client
|
|
537
537
|
// it already holds. The handshake here refreshes the status the User
|
|
@@ -704,7 +704,7 @@ export class McpUserBackendContribution {
|
|
|
704
704
|
// The Connection generation and the credential generation are the same
|
|
705
705
|
// thing for a keyed server, and deliberately are not for an OAuth one: a
|
|
706
706
|
// refresh rotates the sealed generation without disturbing the
|
|
707
|
-
//
|
|
707
|
+
// Capability resolution key a Turn is pinned to.
|
|
708
708
|
expectedGeneration: await this.currentCredentialGeneration(
|
|
709
709
|
input.accountId,
|
|
710
710
|
connection,
|
|
@@ -1518,8 +1518,8 @@ export class McpUserBackendContribution {
|
|
|
1518
1518
|
{ ...connection, state, failure: undefined },
|
|
1519
1519
|
);
|
|
1520
1520
|
// GrokBot's `RemoveMcpAccount`: the server is gone, so its record is
|
|
1521
|
-
// gone with it.
|
|
1522
|
-
//
|
|
1521
|
+
// gone with it. Any Bot that was using the Connection fails closed
|
|
1522
|
+
// when its next Turn resolves the User's enabled capability set.
|
|
1523
1523
|
await this.removeServer(connection.connectionId);
|
|
1524
1524
|
return this.receipt(command.commandId, connection.connectionId);
|
|
1525
1525
|
}
|