@sigrun/ext-mcp 0.2.2 → 0.2.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/CHANGELOG.md +18 -0
- package/dist/annotations.d.ts +36 -0
- package/dist/annotations.js +67 -0
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +9 -0
- package/dist/types.d.ts +13 -0
- package/dist/user-servers.d.ts +13 -0
- package/dist/user-servers.js +31 -2
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.4] - 2026-11-07
|
|
9
|
+
|
|
8
10
|
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- peer pi-durable/pi-ai/chord ~1.5.0 (requires sigrun 0.11.0).
|
|
15
|
+
|
|
16
|
+
## [0.2.3] - 2026-11-05
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Tool annotations kept from `tools/list` and published as the
|
|
21
|
+
`mcp.tool-annotations` hook (readOnlyHint/destructiveHint per tool,
|
|
22
|
+
boolean-only, per-user catalog first then shared servers the calling
|
|
23
|
+
user may reach; unknown tools answer `{unknown: true}`). Downstream
|
|
24
|
+
safety consumers (@sigrun/warden) classify from real metadata instead
|
|
25
|
+
of the MCP default.
|
|
26
|
+
|
|
9
27
|
### Changed
|
|
10
28
|
- Forward-compatible with pi-durable 1.4.0 (explicit `checkpointWhen`;
|
|
11
29
|
widened peer range). No behaviour change.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The `mcp.tool-annotations` published hook: a downstream safety extension
|
|
2
|
+
* asks "what did this tool's server declare about it?" and this extension
|
|
3
|
+
* answers from its live connection maps — the caller's per-user catalog
|
|
4
|
+
* first, then shared servers the caller may reach, else unknown. */
|
|
5
|
+
import { Type, type Static } from "typebox";
|
|
6
|
+
import type { PublishedHook } from "@sigrun/sigrun/extension";
|
|
7
|
+
import type { McpServerConfig } from "./config.js";
|
|
8
|
+
import { type SigrunHost, type ToolAnnotationEntry, type UserCatalog } from "./user-servers.js";
|
|
9
|
+
declare const Input: Type.TObject<{
|
|
10
|
+
tool: Type.TString;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const TOOL_ANNOTATIONS_INPUT: Type.TObject<{
|
|
13
|
+
tool: Type.TString;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const TOOL_ANNOTATIONS_OUTPUT: Type.TUnion<[Type.TObject<{
|
|
16
|
+
server: Type.TString;
|
|
17
|
+
mcpName: Type.TString;
|
|
18
|
+
annotations: Type.TObject<{
|
|
19
|
+
readOnlyHint: Type.TOptional<Type.TBoolean>;
|
|
20
|
+
destructiveHint: Type.TOptional<Type.TBoolean>;
|
|
21
|
+
idempotentHint: Type.TOptional<Type.TBoolean>;
|
|
22
|
+
openWorldHint: Type.TOptional<Type.TBoolean>;
|
|
23
|
+
}>;
|
|
24
|
+
}>, Type.TObject<{
|
|
25
|
+
unknown: Type.TLiteral<true>;
|
|
26
|
+
}>]>;
|
|
27
|
+
export type ToolAnnotationsPayload = Static<typeof Input>;
|
|
28
|
+
export type ToolAnnotationsResult = ToolAnnotationEntry | {
|
|
29
|
+
readonly unknown: true;
|
|
30
|
+
};
|
|
31
|
+
export declare const TOOL_ANNOTATIONS_HOOK = "mcp.tool-annotations";
|
|
32
|
+
export declare function createToolAnnotationsHook(host: SigrunHost, catalog: UserCatalog, shared: ReadonlyMap<string, {
|
|
33
|
+
readonly server: McpServerConfig;
|
|
34
|
+
readonly annotations: ReadonlyMap<string, ToolAnnotationEntry>;
|
|
35
|
+
}>): PublishedHook;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** The `mcp.tool-annotations` published hook: a downstream safety extension
|
|
2
|
+
* asks "what did this tool's server declare about it?" and this extension
|
|
3
|
+
* answers from its live connection maps — the caller's per-user catalog
|
|
4
|
+
* first, then shared servers the caller may reach, else unknown. */
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { sharedServerAllowed, } from "./user-servers.js";
|
|
7
|
+
const Input = Type.Object({
|
|
8
|
+
tool: Type.String({ minLength: 1, maxLength: 512 }),
|
|
9
|
+
});
|
|
10
|
+
const Hints = Type.Object({
|
|
11
|
+
readOnlyHint: Type.Optional(Type.Boolean()),
|
|
12
|
+
destructiveHint: Type.Optional(Type.Boolean()),
|
|
13
|
+
idempotentHint: Type.Optional(Type.Boolean()),
|
|
14
|
+
openWorldHint: Type.Optional(Type.Boolean()),
|
|
15
|
+
});
|
|
16
|
+
const Output = Type.Union([
|
|
17
|
+
Type.Object({
|
|
18
|
+
server: Type.String(),
|
|
19
|
+
mcpName: Type.String(),
|
|
20
|
+
annotations: Hints,
|
|
21
|
+
}),
|
|
22
|
+
Type.Object({ unknown: Type.Literal(true) }),
|
|
23
|
+
]);
|
|
24
|
+
// Exported for the compile round-trip test (the hook exposes the same
|
|
25
|
+
// schemas via `input`/`output`; test code imports these constants to
|
|
26
|
+
// avoid re-deriving them from the wiring).
|
|
27
|
+
export const TOOL_ANNOTATIONS_INPUT = Input;
|
|
28
|
+
export const TOOL_ANNOTATIONS_OUTPUT = Output;
|
|
29
|
+
export const TOOL_ANNOTATIONS_HOOK = "mcp.tool-annotations";
|
|
30
|
+
export function createToolAnnotationsHook(host, catalog, shared) {
|
|
31
|
+
const lookup = async (tool, call) => {
|
|
32
|
+
if (call.user !== undefined) {
|
|
33
|
+
const own = catalog.annotationsFor(call.user).get(tool);
|
|
34
|
+
if (own !== undefined)
|
|
35
|
+
return own;
|
|
36
|
+
// Shared servers lend operator authority, so reachability is gated
|
|
37
|
+
// by the acting user of the conversation; an unidentified caller
|
|
38
|
+
// fails closed, matching sharedServerAllowed's own posture.
|
|
39
|
+
if (call.conversationId !== undefined) {
|
|
40
|
+
for (const [name, entry] of shared) {
|
|
41
|
+
const found = entry.annotations.get(tool);
|
|
42
|
+
if (found === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
if (await sharedServerAllowed(host, call.conversationId, name, entry.server)) {
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// A failed access check is not a reachability grant.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { unknown: true };
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
name: TOOL_ANNOTATIONS_HOOK,
|
|
59
|
+
description: "Resolve a sigrun MCP tool name to its server-declared annotations.",
|
|
60
|
+
input: Input,
|
|
61
|
+
output: Output,
|
|
62
|
+
run: async (payload, call) => {
|
|
63
|
+
const { tool } = payload;
|
|
64
|
+
return await lookup(tool, call);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* per-user entries connect and publish through the structural sigrun host. */
|
|
3
3
|
import { type Extension } from "@earendil-works/pi-durable";
|
|
4
4
|
import { type CatalogSettingsSection, type OwnerEvent, type SigrunHost, type UserService } from "./user-servers.js";
|
|
5
|
+
import type { PublishedHook } from "@sigrun/sigrun/extension";
|
|
5
6
|
export declare const MCP_EXTENSION_NAME = "sigrun.mcp";
|
|
6
7
|
export interface ExtensionRouteContribution {
|
|
7
8
|
readonly method: "POST";
|
|
@@ -22,6 +23,7 @@ export interface McpWiring {
|
|
|
22
23
|
readonly surfaces: {
|
|
23
24
|
readonly settings: readonly CatalogSettingsSection[];
|
|
24
25
|
};
|
|
26
|
+
readonly publishedHooks: readonly PublishedHook[];
|
|
25
27
|
conversationCreated(event: OwnerEvent): Promise<void>;
|
|
26
28
|
runSettled(event: OwnerEvent): Promise<void>;
|
|
27
29
|
onSettingsChanged(keys: readonly string[], user?: string): Promise<void>;
|
package/dist/extension.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createUserOauthFlows, exchangeCode, extractCode, } from "./oauth.js";
|
|
|
7
7
|
import { storeUserMcpToken } from "./tokens.js";
|
|
8
8
|
import { MCP_SERVERS_SECTION_KEY, partitionForSection, renderMcpServersSection, } from "./section.js";
|
|
9
9
|
import { catalogEnabledFromSettings, createUserCatalog, sharedServerAllowed, discoverMcpTools, } from "./user-servers.js";
|
|
10
|
+
import { createToolAnnotationsHook } from "./annotations.js";
|
|
10
11
|
export const MCP_EXTENSION_NAME = "sigrun.mcp";
|
|
11
12
|
/** Standalone tokens need no registry install; the kind/version must match
|
|
12
13
|
* core's session-meta document so its committed owner is readable here. */
|
|
@@ -58,6 +59,7 @@ export function createMcpExtension(host) {
|
|
|
58
59
|
const clients = new Map();
|
|
59
60
|
const serverExposures = new Map();
|
|
60
61
|
const shared = Object.entries(config.servers).filter(([, server]) => server.credentials === "shared" && serverEnabled(server));
|
|
62
|
+
const sharedAnnotations = new Map();
|
|
61
63
|
const catalog = createUserCatalog(host, config.servers, config.userServers ?? "none");
|
|
62
64
|
const oauthFlows = createUserOauthFlows();
|
|
63
65
|
let closed = false;
|
|
@@ -116,12 +118,17 @@ export function createMcpExtension(host) {
|
|
|
116
118
|
return;
|
|
117
119
|
}
|
|
118
120
|
tools.push(...discovered.tools.map((tool) => sharedTool(host, name, server, tool)));
|
|
121
|
+
sharedAnnotations.set(name, {
|
|
122
|
+
server,
|
|
123
|
+
annotations: discovered.annotations,
|
|
124
|
+
});
|
|
119
125
|
if (discovered.exposure !== undefined) {
|
|
120
126
|
serverExposures.set(name, discovered.exposure);
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
catch (cause) {
|
|
124
130
|
clients.delete(name);
|
|
131
|
+
sharedAnnotations.delete(name);
|
|
125
132
|
client.close();
|
|
126
133
|
console.warn(`mcp server "${name}" quarantined during discovery: ${errorMessage(cause)}`);
|
|
127
134
|
}
|
|
@@ -241,6 +248,7 @@ export function createMcpExtension(host) {
|
|
|
241
248
|
clients.clear();
|
|
242
249
|
tools.length = 0;
|
|
243
250
|
serverExposures.clear();
|
|
251
|
+
sharedAnnotations.clear();
|
|
244
252
|
await Promise.all(pending);
|
|
245
253
|
oauthFlows.clear();
|
|
246
254
|
catalog.close();
|
|
@@ -319,6 +327,7 @@ export function createMcpExtension(host) {
|
|
|
319
327
|
routes,
|
|
320
328
|
userServices: catalog.userServices,
|
|
321
329
|
surfaces: { settings: catalog.settings },
|
|
330
|
+
publishedHooks: [createToolAnnotationsHook(host, catalog, sharedAnnotations)],
|
|
322
331
|
conversationCreated: activate,
|
|
323
332
|
runSettled: activate,
|
|
324
333
|
onSettingsChanged: async (keys, user) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/** Wire types for the MCP stdio protocol surface this extension uses. */
|
|
2
2
|
import type { McpServerConfig } from "./config.js";
|
|
3
|
+
/** The MCP-declared hint surface, narrowed to the four boolean hints a
|
|
4
|
+
* downstream safety classifier reads. Non-boolean values are dropped at
|
|
5
|
+
* discovery time rather than trusted blindly. */
|
|
6
|
+
export interface ToolAnnotations {
|
|
7
|
+
readonly readOnlyHint?: boolean;
|
|
8
|
+
readonly destructiveHint?: boolean;
|
|
9
|
+
readonly idempotentHint?: boolean;
|
|
10
|
+
readonly openWorldHint?: boolean;
|
|
11
|
+
}
|
|
3
12
|
/** One tool as `tools/list` reports it. */
|
|
4
13
|
export interface McpToolDefinition {
|
|
5
14
|
readonly name: string;
|
|
6
15
|
readonly description?: string;
|
|
16
|
+
/** Server-advertised hints; only boolean values are retained. */
|
|
17
|
+
readonly annotations?: {
|
|
18
|
+
readonly [key: string]: unknown;
|
|
19
|
+
};
|
|
7
20
|
/** JSON Schema for the tool's arguments object. */
|
|
8
21
|
readonly inputSchema?: {
|
|
9
22
|
readonly type?: unknown;
|
package/dist/user-servers.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { type ConversationId, type ToolRegistration } from "@earendil-works/pi-durable";
|
|
5
5
|
import { type McpClient, type McpServiceHandle } from "./client.js";
|
|
6
6
|
import { type McpServerConfig, type UserServersMode } from "./config.js";
|
|
7
|
+
import type { ToolAnnotations } from "./types.js";
|
|
7
8
|
export interface HandlerContext {
|
|
8
9
|
readonly abortSignal: AbortSignal | undefined;
|
|
9
10
|
}
|
|
@@ -61,9 +62,18 @@ export interface ServerExposure {
|
|
|
61
62
|
readonly description: string;
|
|
62
63
|
readonly count: number;
|
|
63
64
|
}
|
|
65
|
+
/** One discovered tool's annotation record, keyed by the sigrun tool name. */
|
|
66
|
+
export interface ToolAnnotationEntry {
|
|
67
|
+
readonly server: string;
|
|
68
|
+
readonly mcpName: string;
|
|
69
|
+
readonly annotations: ToolAnnotations;
|
|
70
|
+
}
|
|
64
71
|
export interface DiscoveredTools {
|
|
65
72
|
readonly tools: ToolRegistration[];
|
|
66
73
|
readonly exposure: ServerExposure | undefined;
|
|
74
|
+
/** Annotations keyed by the sigrun tool name (`<server>/<tool>`); only
|
|
75
|
+
* tools that advertised at least one boolean hint appear. */
|
|
76
|
+
readonly annotations: ReadonlyMap<string, ToolAnnotationEntry>;
|
|
67
77
|
}
|
|
68
78
|
/** The one discovery-to-registration mapping shared by system and user tools. */
|
|
69
79
|
export declare function discoverMcpTools(name: string, server: McpServerConfig, client: McpClient): Promise<DiscoveredTools>;
|
|
@@ -86,6 +96,9 @@ export interface UserCatalog {
|
|
|
86
96
|
activate(user: string, ctx?: HandlerContext): Promise<void>;
|
|
87
97
|
retire(user: string, server: string): Promise<void>;
|
|
88
98
|
exposures(user: string): ReadonlyMap<string, ServerExposure>;
|
|
99
|
+
/** Live annotation maps for this user's current connections; retirement
|
|
100
|
+
* removes entries because the underlying connections map is the source. */
|
|
101
|
+
annotationsFor(user: string): ReadonlyMap<string, ToolAnnotationEntry>;
|
|
89
102
|
reconcileAll(mode?: UserServersMode): Promise<void>;
|
|
90
103
|
close(): void;
|
|
91
104
|
}
|
package/dist/user-servers.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
/** Per-user catalog lifecycle and the structural sigrun host contract.
|
|
2
2
|
* This package mirrors the host types instead of depending on sigrun, so it
|
|
3
3
|
* remains independently buildable and installable. */
|
|
4
|
-
import { Type } from "
|
|
4
|
+
import { Type } from "typebox";
|
|
5
5
|
import { defineTool, } from "@earendil-works/pi-durable";
|
|
6
6
|
import { connectMcpServer, } from "./client.js";
|
|
7
7
|
import { isStdioServer, parseMcpConfig, resolveToolExposure, serverEnabled, } from "./config.js";
|
|
8
|
+
/** Copy only the four boolean hint values; anything else is untrusted noise. */
|
|
9
|
+
function keepBooleanAnnotations(annotations) {
|
|
10
|
+
if (annotations === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const kept = {};
|
|
13
|
+
for (const hint of [
|
|
14
|
+
"readOnlyHint",
|
|
15
|
+
"destructiveHint",
|
|
16
|
+
"idempotentHint",
|
|
17
|
+
"openWorldHint",
|
|
18
|
+
]) {
|
|
19
|
+
const value = annotations[hint];
|
|
20
|
+
if (typeof value === "boolean")
|
|
21
|
+
kept[hint] = value;
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(kept).length > 0 ? kept : undefined;
|
|
24
|
+
}
|
|
8
25
|
function toContent(block) {
|
|
9
26
|
return block.type === "text" && typeof block.text === "string"
|
|
10
27
|
? { type: "text", text: block.text }
|
|
@@ -14,11 +31,20 @@ function toContent(block) {
|
|
|
14
31
|
export async function discoverMcpTools(name, server, client) {
|
|
15
32
|
const discovered = await client.listTools();
|
|
16
33
|
const tools = [];
|
|
34
|
+
const annotations = new Map();
|
|
17
35
|
const mix = { codemode: false, deferred: false, count: 0 };
|
|
18
36
|
for (const tool of discovered) {
|
|
19
37
|
const exposure = resolveToolExposure(server, tool.name);
|
|
20
38
|
if (exposure === "hidden")
|
|
21
39
|
continue;
|
|
40
|
+
const kept = keepBooleanAnnotations(tool.annotations);
|
|
41
|
+
if (kept !== undefined) {
|
|
42
|
+
annotations.set(`${name}/${tool.name}`, {
|
|
43
|
+
server: name,
|
|
44
|
+
mcpName: tool.name,
|
|
45
|
+
annotations: kept,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
22
48
|
if (exposure === "codemode")
|
|
23
49
|
mix.codemode = true;
|
|
24
50
|
if (exposure === "deferred")
|
|
@@ -61,7 +87,7 @@ export async function discoverMcpTools(name, server, client) {
|
|
|
61
87
|
}));
|
|
62
88
|
}
|
|
63
89
|
if (mix.count === 0 || (!mix.codemode && !mix.deferred)) {
|
|
64
|
-
return { tools, exposure: undefined };
|
|
90
|
+
return { tools, exposure: undefined, annotations };
|
|
65
91
|
}
|
|
66
92
|
const origin = isStdioServer(server)
|
|
67
93
|
? `${server.command} ${server.args.join(" ")}`.trim()
|
|
@@ -72,6 +98,7 @@ export async function discoverMcpTools(name, server, client) {
|
|
|
72
98
|
...mix,
|
|
73
99
|
description: `${mix.count} tool${mix.count === 1 ? "" : "s"} from ${origin}`,
|
|
74
100
|
},
|
|
101
|
+
annotations,
|
|
75
102
|
};
|
|
76
103
|
}
|
|
77
104
|
export function catalogEnabledFromSettings(name, server, settings) {
|
|
@@ -132,6 +159,7 @@ function trackConnection(name, client, discovered) {
|
|
|
132
159
|
return {
|
|
133
160
|
registrations,
|
|
134
161
|
exposure: discovered.exposure,
|
|
162
|
+
annotations: discovered.annotations,
|
|
135
163
|
retire: () => {
|
|
136
164
|
retired = true;
|
|
137
165
|
drain();
|
|
@@ -407,6 +435,7 @@ export function createUserCatalog(host, servers, initialMode = "none") {
|
|
|
407
435
|
retire,
|
|
408
436
|
reconcileAll,
|
|
409
437
|
exposures: (user) => new Map([...(states.get(user)?.connections ?? [])].flatMap(([name, connection]) => connection.exposure === undefined ? [] : [[name, connection.exposure]])),
|
|
438
|
+
annotationsFor: (user) => new Map([...(states.get(user)?.connections ?? [])].flatMap(([, connection]) => [...connection.annotations])),
|
|
410
439
|
close: () => {
|
|
411
440
|
if (closed)
|
|
412
441
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/ext-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "MCP extension for sigrun (pi-durable): registers MCP server tools as <server>/<toolname> tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@earendil-works/chord": "1.
|
|
32
|
-
"@earendil-works/pi-ai": "1.
|
|
33
|
-
"@earendil-works/pi-durable": "1.
|
|
31
|
+
"@earendil-works/chord": "~1.5.0",
|
|
32
|
+
"@earendil-works/pi-ai": "~1.5.0",
|
|
33
|
+
"@earendil-works/pi-durable": "~1.5.0",
|
|
34
34
|
"typebox": "1.3.27"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@earendil-works/chord": "1.
|
|
38
|
-
"@earendil-works/pi-ai": "1.
|
|
39
|
-
"@earendil-works/pi-durable": "1.
|
|
37
|
+
"@earendil-works/chord": "1.5.0",
|
|
38
|
+
"@earendil-works/pi-ai": "1.5.0",
|
|
39
|
+
"@earendil-works/pi-durable": "1.5.0",
|
|
40
40
|
"@sigrun/sigrun": "file:../sigrun",
|
|
41
41
|
"@types/node": "^26.6.4",
|
|
42
42
|
"tsx": "^4.23.15",
|