@geohar/pi-mcp-combiner 0.14.3 → 0.16.0-dev.1

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.
Files changed (98) hide show
  1. package/README.md +71 -48
  2. package/dist/client/config-ladder.d.ts +1 -1
  3. package/dist/client/config-ladder.d.ts.map +1 -1
  4. package/dist/client/control.d.ts +1 -1
  5. package/dist/client/control.d.ts.map +1 -1
  6. package/dist/client/elicitation.d.ts +4 -2
  7. package/dist/client/elicitation.d.ts.map +1 -1
  8. package/dist/client/elicitation.js +8 -2
  9. package/dist/client/elicitation.js.map +1 -1
  10. package/dist/client/footer.d.ts +1 -1
  11. package/dist/client/footer.d.ts.map +1 -1
  12. package/dist/client/panel.d.ts +1 -1
  13. package/dist/client/panel.d.ts.map +1 -1
  14. package/dist/client/panel.js +1 -1
  15. package/dist/client/panel.js.map +1 -1
  16. package/dist/client/prompts.d.ts +1 -1
  17. package/dist/client/prompts.d.ts.map +1 -1
  18. package/dist/client/proxy-tool.d.ts +1 -1
  19. package/dist/client/proxy-tool.d.ts.map +1 -1
  20. package/dist/client/resource-naming.d.ts +8 -0
  21. package/dist/client/resource-naming.d.ts.map +1 -0
  22. package/dist/client/resource-naming.js +33 -0
  23. package/dist/client/resource-naming.js.map +1 -0
  24. package/dist/client/settings.d.ts +5 -4
  25. package/dist/client/settings.d.ts.map +1 -1
  26. package/dist/client/settings.js +3 -3
  27. package/dist/client/settings.js.map +1 -1
  28. package/dist/client/tool-matching.d.ts +32 -0
  29. package/dist/client/tool-matching.d.ts.map +1 -0
  30. package/dist/client/tool-matching.js +43 -0
  31. package/dist/client/tool-matching.js.map +1 -0
  32. package/dist/client/types.d.ts +71 -0
  33. package/dist/client/types.d.ts.map +1 -0
  34. package/dist/client/types.js +16 -0
  35. package/dist/client/types.js.map +1 -0
  36. package/dist/client/widget-support.d.ts +17 -0
  37. package/dist/client/widget-support.d.ts.map +1 -0
  38. package/dist/client/widget-support.js +89 -0
  39. package/dist/client/widget-support.js.map +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +109 -53
  42. package/dist/index.js.map +1 -1
  43. package/dist/{client/connection.d.ts → native/combiner-connection.d.ts} +46 -38
  44. package/dist/native/combiner-connection.d.ts.map +1 -0
  45. package/dist/native/combiner-connection.js +412 -0
  46. package/dist/native/combiner-connection.js.map +1 -0
  47. package/dist/native/index.d.ts +30 -0
  48. package/dist/native/index.d.ts.map +1 -0
  49. package/dist/native/index.js +61 -0
  50. package/dist/native/index.js.map +1 -0
  51. package/dist/native/resources.d.ts +18 -0
  52. package/dist/native/resources.d.ts.map +1 -0
  53. package/dist/native/resources.js +97 -0
  54. package/dist/native/resources.js.map +1 -0
  55. package/dist/native/tool-surface.d.ts +26 -0
  56. package/dist/native/tool-surface.d.ts.map +1 -0
  57. package/dist/native/tool-surface.js +113 -0
  58. package/dist/native/tool-surface.js.map +1 -0
  59. package/dist/pi.d.ts +19 -0
  60. package/dist/pi.d.ts.map +1 -1
  61. package/package.json +3 -5
  62. package/src/client/config-ladder.ts +1 -1
  63. package/src/client/control.ts +1 -1
  64. package/src/client/elicitation.ts +10 -2
  65. package/src/client/footer.ts +1 -1
  66. package/src/client/panel.ts +2 -2
  67. package/src/client/prompts.ts +1 -1
  68. package/src/client/proxy-tool.ts +1 -1
  69. package/src/client/resource-naming.ts +36 -0
  70. package/src/client/settings.ts +7 -6
  71. package/src/client/tool-matching.ts +69 -0
  72. package/src/client/types.ts +80 -0
  73. package/src/client/widget-support.ts +86 -0
  74. package/src/index.ts +109 -65
  75. package/src/native/combiner-connection.ts +483 -0
  76. package/src/native/index.ts +100 -0
  77. package/src/native/resources.ts +112 -0
  78. package/src/native/tool-surface.ts +147 -0
  79. package/src/pi.ts +16 -0
  80. package/dist/client/connection.d.ts.map +0 -1
  81. package/dist/client/connection.js +0 -332
  82. package/dist/client/connection.js.map +0 -1
  83. package/dist/client/direct-tools.d.ts +0 -41
  84. package/dist/client/direct-tools.d.ts.map +0 -1
  85. package/dist/client/direct-tools.js +0 -122
  86. package/dist/client/direct-tools.js.map +0 -1
  87. package/dist/client/resources.d.ts +0 -20
  88. package/dist/client/resources.d.ts.map +0 -1
  89. package/dist/client/resources.js +0 -119
  90. package/dist/client/resources.js.map +0 -1
  91. package/dist/client/script.d.ts +0 -10
  92. package/dist/client/script.d.ts.map +0 -1
  93. package/dist/client/script.js +0 -130
  94. package/dist/client/script.js.map +0 -1
  95. package/src/client/connection.ts +0 -373
  96. package/src/client/direct-tools.ts +0 -146
  97. package/src/client/resources.ts +0 -137
  98. package/src/client/script.ts +0 -150
@@ -0,0 +1,32 @@
1
+ /** A combined-tool allowlist spec from the ladder's `combiner.directTools` key:
2
+ * a glob allowlist, or "search" (legacy adapter semantics — deprecated in the
3
+ * native mode, where tool_search replaces it). */
4
+ export type DirectToolsSpec = string[] | "search";
5
+ /** Names that must never be shadowed by a promoted/registered tool (pi builtins).
6
+ * Callers add their own tool names on top. */
7
+ export declare const RESERVED_TOOL_NAMES: readonly ["read", "bash", "edit", "write", "grep", "find", "ls", "mcp", "glob"];
8
+ /** Apply a server allow/deny filter to a combined tool list. Server names may
9
+ * contain underscores (gws_georgeharker), so matching is by PREFIX — a tool
10
+ * belongs to entry E iff its name starts `E_` (subsumes first-segment equality). */
11
+ export declare function toolServerMatches(toolName: string, serverEntry: string): boolean;
12
+ /** Apply a server allow/deny filter to a combined tool list (`<server>_` prefix,
13
+ * underscore-safe via toolServerMatches). */
14
+ export declare function applyServerFilter(tools: ToolSummary[], filter: {
15
+ allow?: string[];
16
+ deny?: string[];
17
+ } | undefined): ToolSummary[];
18
+ /** fnmatch-lite: `*` → any run, `?` → one char, everything else literal. */
19
+ export declare function globToRegExp(pattern: string): RegExp;
20
+ export declare function matchesGlob(name: string, pattern: string): boolean;
21
+ export type ToolSummary = {
22
+ name: string;
23
+ description?: string;
24
+ inputSchema?: unknown;
25
+ annotations?: {
26
+ readOnlyHint?: boolean;
27
+ destructiveHint?: boolean;
28
+ idempotentHint?: boolean;
29
+ openWorldHint?: boolean;
30
+ };
31
+ };
32
+ //# sourceMappingURL=tool-matching.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-matching.d.ts","sourceRoot":"","sources":["../../src/client/tool-matching.ts"],"names":[],"mappings":"AAUA;;mDAEmD;AACnD,MAAM,MAAM,eAAe,GAAG,MAAM,EAAE,GAAG,QAAQ,CAAA;AAEjD;+CAC+C;AAC/C,eAAO,MAAM,mBAAmB,iFAAkF,CAAA;AAElH;;qFAEqF;AACrF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED;8CAC8C;AAC9C,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,WAAW,EAAE,EACpB,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,GAC1D,WAAW,EAAE,CASf;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAElE;AAKD,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE;QACV,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,eAAe,CAAC,EAAE,OAAO,CAAA;QACzB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;KAC1B,CAAA;CACJ,CAAA"}
@@ -0,0 +1,43 @@
1
+ // Shared tool-name matching helpers — extracted from legacy-client/direct-tools.ts
2
+ // during the legacy-client factoring so the NATIVE tool surface (src/native/) and the
3
+ // client half (src/client/: config-ladder, panel) can use them without importing the
4
+ // deprecated legacy-client/ directory. When legacy-client/ is deleted, this file
5
+ // survives untouched.
6
+ //
7
+ // These are the stable contract:
8
+ // - tool names arrive with the combiner's baked `<server>_` prefix
9
+ // - server attribution is PREFIX-based (underscore-safe), not string equality
10
+ /** Names that must never be shadowed by a promoted/registered tool (pi builtins).
11
+ * Callers add their own tool names on top. */
12
+ export const RESERVED_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls", "mcp", "glob"];
13
+ /** Apply a server allow/deny filter to a combined tool list. Server names may
14
+ * contain underscores (gws_georgeharker), so matching is by PREFIX — a tool
15
+ * belongs to entry E iff its name starts `E_` (subsumes first-segment equality). */
16
+ export function toolServerMatches(toolName, serverEntry) {
17
+ return toolName.startsWith(`${serverEntry}_`);
18
+ }
19
+ /** Apply a server allow/deny filter to a combined tool list (`<server>_` prefix,
20
+ * underscore-safe via toolServerMatches). */
21
+ export function applyServerFilter(tools, filter) {
22
+ if (!filter)
23
+ return tools;
24
+ if (filter.allow?.length) {
25
+ return tools.filter((t) => filter.allow.some((e) => toolServerMatches(t.name, e)));
26
+ }
27
+ if (filter.deny?.length) {
28
+ return tools.filter((t) => !filter.deny.some((e) => toolServerMatches(t.name, e)));
29
+ }
30
+ return tools;
31
+ }
32
+ /** fnmatch-lite: `*` → any run, `?` → one char, everything else literal. */
33
+ export function globToRegExp(pattern) {
34
+ const escaped = pattern
35
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
36
+ .replace(/\*/g, ".*")
37
+ .replace(/\?/g, ".");
38
+ return new RegExp(`^${escaped}$`, "i");
39
+ }
40
+ export function matchesGlob(name, pattern) {
41
+ return globToRegExp(pattern).test(name);
42
+ }
43
+ //# sourceMappingURL=tool-matching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-matching.js","sourceRoot":"","sources":["../../src/client/tool-matching.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,sFAAsF;AACtF,qFAAqF;AACrF,iFAAiF;AACjF,sBAAsB;AACtB,EAAE;AACF,iCAAiC;AACjC,qEAAqE;AACrE,gFAAgF;AAOhF;+CAC+C;AAC/C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA;AAElH;;qFAEqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,WAAmB;IACnE,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,CAAC,CAAA;AACjD,CAAC;AAED;8CAC8C;AAC9C,MAAM,UAAU,iBAAiB,CAC7B,KAAoB,EACpB,MAAyD;IAEzD,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACvF,CAAC;IACD,OAAO,KAAK,CAAA;AAChB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,OAAe;IACxC,MAAM,OAAO,GAAG,OAAO;SAClB,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC;SACpC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACxB,OAAO,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,EAAE,GAAG,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAAe;IACrD,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC3C,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { ServerFilter } from "./config-ladder.js";
2
+ import type { ElicitUi } from "./elicitation.js";
3
+ export type LogFn = (level: "info" | "warn" | "error", message: string) => void;
4
+ export type ResolvedConnection = {
5
+ /** Base URL including /mcp, WITHOUT any token path (e.g. http://127.0.0.1:9741/mcp). */
6
+ baseUrl: string;
7
+ /** Env var holding the inbound bearer token (read at connect time), if any. */
8
+ bearerTokenEnv?: string;
9
+ /** Explicit token already present in the configured URL path (user override). */
10
+ urlToken?: string;
11
+ };
12
+ export type ConnectionState = "disconnected" | "connecting" | "connected" | "failed";
13
+ export type ToolSummary = {
14
+ name: string;
15
+ description?: string;
16
+ inputSchema?: unknown;
17
+ };
18
+ export type PromptArgument = {
19
+ name: string;
20
+ description?: string;
21
+ required?: boolean;
22
+ };
23
+ export type PromptSummary = {
24
+ name: string;
25
+ title?: string;
26
+ description?: string;
27
+ arguments?: PromptArgument[];
28
+ };
29
+ export type ResourceSummary = {
30
+ uri: string;
31
+ name?: string;
32
+ description?: string;
33
+ mimeType?: string;
34
+ _meta?: Record<string, unknown>;
35
+ };
36
+ /** Optional callbacks fired by the connection (set from index.ts wiring). */
37
+ export type ConnectionHooks = {
38
+ /** tools/list_changed arrived — caches invalidated; refresh derived surfaces. */
39
+ onToolsChanged?: () => void;
40
+ /** connection state transitioned — refresh footer/status surfaces. */
41
+ onStateChange?: (state: ConnectionState) => void;
42
+ /** Stage 2: the combiner announced a widget UI URL while an agent tool call
43
+ * is held in flight — the client should open it (gated by uiAutoOpen). */
44
+ onWidgetUrl?: (url: string) => void;
45
+ };
46
+ /** The connection surface every module types against — the duck-parity contract
47
+ * both the retired SDK connection and the native pi-mcp connection implemented. */
48
+ export interface CombinerConnection {
49
+ state: ConnectionState;
50
+ lastError: string | undefined;
51
+ sessionToken: string | undefined;
52
+ setToken(token: string): void;
53
+ rebind(conn: ResolvedConnection): boolean;
54
+ reset(reason: string): Promise<void>;
55
+ ensureConnected(): Promise<unknown>;
56
+ listTools(force?: boolean): Promise<ToolSummary[]>;
57
+ listPrompts(force?: boolean): Promise<PromptSummary[]>;
58
+ getPrompt(name: string, args: Record<string, string>): Promise<unknown>;
59
+ listResources(force?: boolean): Promise<ResourceSummary[]>;
60
+ readResource(uri: string): Promise<unknown>;
61
+ callTool(name: string, args: Record<string, unknown> | undefined): Promise<unknown>;
62
+ controlOrigin(): string;
63
+ uiUrlFor(resourceUri: string): string;
64
+ applyFilter(filter: ServerFilter): Promise<void>;
65
+ health(): Promise<unknown>;
66
+ setHooks(hooks: ConnectionHooks): void;
67
+ bindUi(ui: ElicitUi): void;
68
+ }
69
+ /** Build the tokened endpoint URL: <origin>/mcp/<token> (combiner's URL form). */
70
+ export declare function tokenedUrl(baseUrl: string, token: string): string;
71
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ // Shared connection contract: the types every surface (router, panel, footer,
2
+ // prompts, resources, the extension wiring) types against. The concrete class
3
+ // lives in native/combiner-connection.ts (the pi-mcp transport); this module
4
+ // owns the INTERFACE so no surface depends on the implementation.
5
+ /** Build the tokened endpoint URL: <origin>/mcp/<token> (combiner's URL form). */
6
+ export function tokenedUrl(baseUrl, token) {
7
+ try {
8
+ const u = new URL(baseUrl);
9
+ u.pathname = `${u.pathname.replace(/\/+$/, "")}/${token}`;
10
+ return u.toString();
11
+ }
12
+ catch {
13
+ return `${baseUrl.replace(/\/+$/, "")}/${token}`;
14
+ }
15
+ }
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,kEAAkE;AAmElE,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,KAAa;IACrD,IAAI,CAAC;QACD,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;QAC1B,CAAC,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAA;QACzD,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAA;IACpD,CAAC;AACL,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { ResourceSummary } from "./types.js";
2
+ export declare const MAX_RESULT_CHARS: number;
3
+ export declare const MCP_APP_MIME = "text/html;profile=mcp-app";
4
+ /** True for interactive ext-apps resources: read_* returns their markup AND
5
+ * surfaces the combiner UI-host URL (which serves the live widget). */
6
+ export declare function isInteractiveResource(r: ResourceSummary): boolean;
7
+ /** Platform browser open — best-effort; failures are fine (the URL is in the
8
+ * result text anyway). Spawned detached so pi退出 doesn't kill the browser. */
9
+ export declare function openInBrowser(url: string): void;
10
+ /** The ui:// resource a tool result carries (port of the adapter's
11
+ * getToolUiResourceUri): `_meta["ui/resourceUri"]` or `_meta.ui.resourceUri`. */
12
+ export declare function toolUiResourceUri(result: unknown): string | undefined;
13
+ /** MCP readResource result → guarded plain text: text contents pass through
14
+ * (pretty-printed when small JSON; with the shared size guard), images become
15
+ * markers, base64 blobs become byte notes. */
16
+ export declare function renderResourceResult(result: unknown): string;
17
+ //# sourceMappingURL=widget-support.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-support.d.ts","sourceRoot":"","sources":["../../src/client/widget-support.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,eAAO,MAAM,gBAAgB,QAAY,CAAA;AAEzC,eAAO,MAAM,YAAY,8BAA8B,CAAA;AAEvD;wEACwE;AACxE,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAEjE;AAED;8EAC8E;AAC9E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO/C;AAED;iFACiF;AACjF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOrE;AAiBD;;+CAE+C;AAC/C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAsB5D"}
@@ -0,0 +1,89 @@
1
+ // Widget/interactive-resource support — extracted from legacy-client/render.ts,
2
+ // proxy-tool.ts, and resources.ts during the legacy-client factoring.
3
+ //
4
+ // These pieces are NOT proxy-surface specific: they are the combiner UI-host,
5
+ // mcp-app-resource, and browser-open mechanics that SURVIVE into the native mode
6
+ // (widget holds via the connection's onWidgetUrl log-tap; read_* tools for ui://
7
+ // resources, which pi's built-in read_mcp_resource deliberately does NOT cover).
8
+ // When legacy-client/ is deleted, this file keeps them alive; legacy/resources.ts
9
+ // imports them from here in the meantime.
10
+ import { spawn } from "node:child_process";
11
+ export const MAX_RESULT_CHARS = 16 * 1024;
12
+ export const MCP_APP_MIME = "text/html;profile=mcp-app";
13
+ /** True for interactive ext-apps resources: read_* returns their markup AND
14
+ * surfaces the combiner UI-host URL (which serves the live widget). */
15
+ export function isInteractiveResource(r) {
16
+ return r.mimeType === MCP_APP_MIME || r.uri.startsWith("ui://");
17
+ }
18
+ /** Platform browser open — best-effort; failures are fine (the URL is in the
19
+ * result text anyway). Spawned detached so pi退出 doesn't kill the browser. */
20
+ export function openInBrowser(url) {
21
+ const cmd = process.platform === "darwin" ? ["open"] : ["xdg-open"];
22
+ try {
23
+ spawn(cmd[0], [...cmd.slice(1), url], { stdio: "ignore", detached: true }).unref?.();
24
+ }
25
+ catch {
26
+ // no opener available — the URL is in the result text anyway
27
+ }
28
+ }
29
+ /** The ui:// resource a tool result carries (port of the adapter's
30
+ * getToolUiResourceUri): `_meta["ui/resourceUri"]` or `_meta.ui.resourceUri`. */
31
+ export function toolUiResourceUri(result) {
32
+ const meta = result?._meta;
33
+ if (typeof meta !== "object" || meta === null)
34
+ return undefined;
35
+ const m = meta;
36
+ const nested = m.ui?.resourceUri;
37
+ const uri = typeof nested === "string" ? nested : m["ui/resourceUri"];
38
+ return typeof uri === "string" && uri.startsWith("ui://") ? uri : undefined;
39
+ }
40
+ const maxPrettyCharsDefault = 2048;
41
+ function prettifyIfSmallJson(text, maxPrettyChars = maxPrettyCharsDefault) {
42
+ const trimmed = text.trim();
43
+ if (trimmed.length === 0 || trimmed.length > maxPrettyChars)
44
+ return text;
45
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("[")))
46
+ return text;
47
+ try {
48
+ const parsed = JSON.parse(trimmed);
49
+ if (typeof parsed !== "object" || parsed === null)
50
+ return text;
51
+ return JSON.stringify(parsed, null, 2);
52
+ }
53
+ catch {
54
+ return text;
55
+ }
56
+ }
57
+ /** MCP readResource result → guarded plain text: text contents pass through
58
+ * (pretty-printed when small JSON; with the shared size guard), images become
59
+ * markers, base64 blobs become byte notes. */
60
+ export function renderResourceResult(result) {
61
+ const contents = result?.contents;
62
+ if (!Array.isArray(contents) || contents.length === 0)
63
+ return "(empty resource)";
64
+ const parts = [];
65
+ for (const c of contents) {
66
+ if (typeof c !== "object" || c === null)
67
+ continue;
68
+ const item = c;
69
+ const mime = typeof item.mimeType === "string" ? item.mimeType : "unknown";
70
+ if (typeof item.text === "string") {
71
+ parts.push(prettifyIfSmallJson(item.text));
72
+ }
73
+ else if (typeof item.blob === "string") {
74
+ const bytes = Math.floor((item.blob.length * 3) / 4); // base64 → approx decoded size
75
+ parts.push(`[binary ${mime}, ~${bytes} bytes — uri ${item.uri ?? "?"}]`);
76
+ }
77
+ else if (mime.startsWith("image/")) {
78
+ parts.push(`[image: ${mime} — uri ${item.uri ?? "?"}]`);
79
+ }
80
+ else {
81
+ parts.push(`[unreadable content: ${mime} — uri ${item.uri ?? "?"}]`);
82
+ }
83
+ }
84
+ const text = parts.join("\n").trim() || "(empty resource)";
85
+ if (text.length <= MAX_RESULT_CHARS)
86
+ return text;
87
+ return `${text.slice(0, MAX_RESULT_CHARS)}\n\n… (resource truncated at ${MAX_RESULT_CHARS} chars)`;
88
+ }
89
+ //# sourceMappingURL=widget-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-support.js","sourceRoot":"","sources":["../../src/client/widget-support.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAER,YAAY,EAKf,MAAM,SAAS,CAAA;AA4PhB,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8b1D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAER,YAAY,EAMf,MAAM,SAAS,CAAA;AAyPhB,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4e1D"}
package/dist/index.js CHANGED
@@ -31,17 +31,20 @@ import { homedir } from "node:os";
31
31
  import { basename, dirname, join } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
  import { resolveSharedserver } from "./sharedserver-resolve.js";
34
- import { CombinerConnection } from "./client/connection.js";
35
- import { createMcpTool, openInBrowser } from "./client/proxy-tool.js";
34
+ import { NativeCombinerConnection, activateNativeMode } from "./native/index.js";
35
+ // ── TOOL SURFACE ───────────────────────────────────────────────────────────────
36
+ // The mcp router (search/describe/call on the owned connection) + the native
37
+ // per-tool registration (pi-1.0 exposure/namespace/annotations) + interactive
38
+ // read_<resource> tools. Shared surface modules live in src/client/; the transport
39
+ // is src/native/combiner-connection.ts (pi-mcp, peer dep).
40
+ import { createMcpTool } from "./client/proxy-tool.js";
41
+ import { openInBrowser } from "./client/widget-support.js";
36
42
  import { agentDir, loadSettings, settingsPath } from "./client/settings.js";
37
43
  import { resolveSessionConfig } from "./client/config-ladder.js";
38
44
  import { metaToolCall, statusText as controlStatusText } from "./client/control.js";
39
45
  import { openCombinerPanel } from "./client/panel.js";
40
46
  import { syncPromptCommands } from "./client/prompts.js";
41
- import { syncResourceTools } from "./client/resources.js";
42
- import { activateFromSearch, applyServerFilter, RESERVED_TOOL_NAMES, syncAllowlistTools, } from "./client/direct-tools.js";
43
47
  import { updateFooter } from "./client/footer.js";
44
- import { createScriptTool } from "./client/script.js";
45
48
  const DEFAULT_PORT = 9741;
46
49
  const DEFAULT_NAME = "mcp-combiner";
47
50
  const DEFAULT_GRACE = "30m";
@@ -288,13 +291,25 @@ export default function mcpCombiner(pi) {
288
291
  uiLevel = "warn";
289
292
  else if (level === "error")
290
293
  uiLevel = "error";
291
- uiNotify(line, uiLevel);
294
+ try {
295
+ uiNotify(line, uiLevel);
296
+ }
297
+ catch {
298
+ // The captured ctx went stale (reload/session replacement kills
299
+ // extension ctxs — pi 1.0 asserts on use). Degrade to stderr and
300
+ // drop the capture so later logs skip the dead path entirely:
301
+ // an async log line (e.g. a superseded connection's reset) must
302
+ // NEVER crash the process.
303
+ uiNotify = undefined;
304
+ process.stderr.write(`${line}\n`);
305
+ }
292
306
  }
293
307
  else if (level !== "info") {
294
308
  process.stderr.write(`${line}\n`);
295
309
  }
296
310
  };
297
311
  let connection;
312
+ let nativeSurface;
298
313
  // Session-config HOLDER: everything the ladder shapes, resolved against a SESSION
299
314
  // cwd (worktree subagents / project switches get their own project layers).
300
315
  // Factory-time resolution with process.cwd() is the pre-session default; every
@@ -306,17 +321,6 @@ export default function mcpCombiner(pi) {
306
321
  bearerTokenEnv: cfg.bearerTokenEnv ?? "MCP_COMBINER_AUTH_TOKEN",
307
322
  urlToken: cfg.urlToken,
308
323
  });
309
- const registeredDirect = new Set();
310
- const directReserved = new Set(RESERVED_TOOL_NAMES);
311
- directReserved.add(toolName);
312
- directReserved.add(`${toolName}Script`);
313
- const directDepsFor = (conn) => ({
314
- connection: conn,
315
- registered: registeredDirect,
316
- reserved: directReserved,
317
- register: (tool) => pi.registerTool(tool),
318
- log: clientLog,
319
- });
320
324
  if (adapterEnabled) {
321
325
  sessionCfg = resolveSessionConfig({
322
326
  cwd: process.cwd(),
@@ -324,17 +328,47 @@ export default function mcpCombiner(pi) {
324
328
  settingsUrl: settings.url,
325
329
  defaultUrl: defaultCombinerUrl(),
326
330
  });
327
- connection = new CombinerConnection(connInputsFor(sessionCfg), clientLog);
328
- const activeConn = connection; // narrowed for the activation closure below
331
+ // Zombie teardown: pi re-imports the extension module on /reload WITHOUT
332
+ // firing session_shutdown (the session persists, no unload hook exists), so
333
+ // the previous instance's McpClient + SSE stream would live on and compete
334
+ // for the token's elicit routing. A process-global registry survives module
335
+ // re-imports: the fresh factory closes every prior live connection first.
336
+ const g = globalThis;
337
+ // The teardown closures below (from prior factory runs, orphaned by pi's
338
+ // reload-without-session_shutdown) know how to fully retire their instance:
339
+ // clear the footer timer, drop the captured UI ctx (stale-ctx reads on it
340
+ // are a hard pi-1.0 assertion), and close the connection.
341
+ for (const dispose of g.__piMcpCombinerDispose ?? []) {
342
+ try {
343
+ dispose();
344
+ }
345
+ catch {
346
+ // teardown must never break the fresh instance's load
347
+ }
348
+ }
349
+ g.__piMcpCombinerDispose = [];
350
+ connection = new NativeCombinerConnection(connInputsFor(sessionCfg), clientLog);
351
+ // The mcp router registers alongside the per-tool native surface (the same
352
+ // owned connection — not a second proxy): tool_call gates such as
353
+ // pi-permission-system and pi-subagents recognize MCP calls by the tool
354
+ // name "mcp", and the router keeps a discovery/search surface.
329
355
  pi.registerTool(createMcpTool({
330
356
  connection,
331
357
  toolName,
332
358
  getServerFilter: () => sessionCfg?.serverFilter,
333
- onSearchMatches: (tools) => sessionCfg?.directSpec === "search" ? activateFromSearch(tools, directDepsFor(activeConn)) : [],
334
359
  }));
335
- if (settings.scriptMode) {
336
- pi.registerTool(createScriptTool({ connection, name: `${toolName}Script` }));
337
- }
360
+ // Per-tool surface + interactive read_*. The FIRST registration runs at
361
+ // session_start (post-setToken — the connection refuses to connect
362
+ // without a grouping token); list_changed re-syncs ride resyncAll below.
363
+ nativeSurface = activateNativeMode(pi, {
364
+ connection,
365
+ directSpec: sessionCfg?.directSpec ?? [],
366
+ serverFilter: sessionCfg?.serverFilter,
367
+ exposeResources: settings.exposeResources !== false,
368
+ uiAutoOpen: settings.uiAutoOpen !== false,
369
+ warnLargeDirectExposure: settings.warnLargeDirectExposure,
370
+ log: clientLog,
371
+ });
338
372
  if (sessionCfg.otherServers.length && !adapterPackageInstalled()) {
339
373
  clientLog("info", `note: ${sessionCfg.otherServers.length} non-combiner server(s) in the mcp.json ladder are not ` +
340
374
  `connected by this extension (${sessionCfg.otherServers.join(", ")})`);
@@ -350,9 +384,22 @@ export default function mcpCombiner(pi) {
350
384
  if (connection) {
351
385
  const conn = connection; // captured non-optional for the handlers below
352
386
  const registeredPrompts = new Set();
353
- const registeredResources = new Set();
354
387
  let footerTimer;
355
388
  let uiCtx;
389
+ // Full instance teardown, callable from a LATER factory run (pi re-imports
390
+ // the module on /reload without firing session_shutdown): clears this
391
+ // instance's footer timer, drops its captured UI ctx (stale-ctx reads are
392
+ // a hard pi-1.0 assertion), and closes its connection. The registry lives
393
+ // on globalThis — the one thing that survives a module re-import.
394
+ const teardownGlobal = globalThis;
395
+ (teardownGlobal.__piMcpCombinerDispose ??= []).push(() => {
396
+ if (footerTimer)
397
+ clearInterval(footerTimer);
398
+ footerTimer = undefined;
399
+ uiCtx = undefined;
400
+ uiNotify = undefined;
401
+ void conn.reset("superseded by a newer extension instance (reload)");
402
+ });
356
403
  const refreshFooter = () => {
357
404
  if (uiCtx)
358
405
  void updateFooter(conn, uiCtx, settings.mcpFooterKey, settings.mcpFooterStatus).catch(() => undefined);
@@ -362,31 +409,27 @@ export default function mcpCombiner(pi) {
362
409
  return;
363
410
  void syncPromptCommands(pi, conn, toolName, registeredPrompts, (m, l) => clientLog(l, m)).catch(() => undefined);
364
411
  };
365
- const syncResources = () => {
366
- if (!settings.exposeResources)
367
- return;
368
- void syncResourceTools((tool) => pi.registerTool(tool), conn, registeredResources, sessionCfg?.serverFilter, (m, l) => clientLog(l, m)).catch(() => undefined);
369
- };
370
- const syncDirect = () => {
371
- // Allowlist mode only — "search" registers nothing upfront.
372
- const spec = sessionCfg?.directSpec;
373
- if (!Array.isArray(spec) || spec.length === 0)
374
- return;
375
- void conn
376
- .listTools()
377
- .then((tools) => {
378
- syncAllowlistTools(applyServerFilter(tools, sessionCfg?.serverFilter), spec, directDepsFor(conn));
379
- })
380
- .catch(() => undefined);
412
+ const resyncAll = () => {
413
+ // Prompts + footer re-sync on: list_changed notifications, reconnect
414
+ // catch-up (fired by the connection itself), AND session_start — a
415
+ // reload/re-enter with an UNCHANGED token skips any reconnect, so
416
+ // without this the surfaces would only ever update on notifications.
417
+ nativeSurface?.run();
418
+ syncPrompts();
419
+ refreshFooter();
381
420
  };
382
421
  conn.setHooks({
383
- onToolsChanged: () => {
384
- syncPrompts();
385
- syncResources();
386
- syncDirect();
422
+ onToolsChanged: resyncAll,
423
+ onStateChange: (state) => {
424
+ // Re-attach on (re)connect: a combiner bounce kills the instance our
425
+ // sharedserver reference was attached to, and nothing else re-takes
426
+ // ownership until the next session_start — an orphaned combiner then
427
+ // grace-stops 30m later (sharedserver `use` is idempotent per pid, so
428
+ // re-attaching on every connect is safe and cheap).
429
+ if (state === "connected")
430
+ attachCombiner(clientLog);
387
431
  refreshFooter();
388
432
  },
389
- onStateChange: () => refreshFooter(),
390
433
  // Stage 2: the widget hold announces its UI URL mid-call — open it.
391
434
  onWidgetUrl: (url) => {
392
435
  if (settings.uiAutoOpen !== false)
@@ -408,6 +451,11 @@ export default function mcpCombiner(pi) {
408
451
  });
409
452
  conn.rebind(connInputsFor(sessionCfg));
410
453
  conn.setToken(sessionToken(event, ctx));
454
+ // The native per-tool/resource registration runs HERE — post-setToken,
455
+ // the first moment the grouping token exists (a factory-time run
456
+ // would fail with "no grouping token set"). Subsequent re-syncs ride
457
+ // resyncAll via onToolsChanged.
458
+ nativeSurface?.run();
411
459
  // Per-project exposure: pending filters apply server-side before first connect.
412
460
  const filter = sessionCfg.serverFilter;
413
461
  if (filter) {
@@ -431,8 +479,6 @@ export default function mcpCombiner(pi) {
431
479
  .then(() => {
432
480
  refreshFooter();
433
481
  syncPrompts();
434
- syncResources();
435
- syncDirect();
436
482
  })
437
483
  .catch(() => undefined);
438
484
  }
@@ -475,7 +521,7 @@ export default function mcpCombiner(pi) {
475
521
  // restart-server <srv> (combiner meta-tools through our client), system-prompt
476
522
  // (show the injected directive), install-config [path] (write the shared mcp.json
477
523
  // combiner entry — legacy pairing with pi-mcp-adapter, now also read by our ladder).
478
- pi.registerCommand("mcp-combiner", {
524
+ const combinerCommandSpec = {
479
525
  description: "mcp-combiner — verbs: status, enable <srv>, disable <srv>, restart-server <srv>, system-prompt, install-config [path]",
480
526
  getArgumentCompletions: (prefix) => completeVerbs(prefix),
481
527
  handler: (args, ctx) => {
@@ -530,7 +576,15 @@ export default function mcpCombiner(pi) {
530
576
  }
531
577
  ctx.ui?.notify?.(`mcp-combiner: unknown verb "${v}". Try: ${COMMAND_VERBS.join(", ")}`, "warn");
532
578
  },
533
- });
579
+ };
580
+ pi.registerCommand("mcp-combiner", combinerCommandSpec);
581
+ // NOTE: we deliberately do NOT register "/mcp" — pi's built-in MCP extension
582
+ // owns that command and stays the native-mode transport; our surfaces ("mcp"
583
+ // router tool, direct <server>_<tool>, read_*) don't collide with the builtin's
584
+ // (codemode / tool-search / read_mcp_resource). Registering "/mcp" would
585
+ // displace the builtin at load (pi's loader drops replaceable built-ins on
586
+ // command-name collision) and with it the native support we're migrating
587
+ // toward.
534
588
  if (hostOwned || !manage) {
535
589
  // Registration is static (mcp.json) and the combiner is someone else's to run:
536
590
  // nothing to launch. The instructions handler above still applies.
@@ -540,10 +594,7 @@ export default function mcpCombiner(pi) {
540
594
  // Pi forbids starting background resources from the factory, so all of this is
541
595
  // deferred to session_start. session_start fires again on reload/new/resume/fork
542
596
  // within the same process; the `attachment` guard makes re-entry a no-op attach.
543
- pi.on("session_start", (_event, ctx) => {
544
- if (attachment)
545
- return; // already attached in this process
546
- const log = makeLog(ctx, notify);
597
+ const attachCombiner = (log) => {
547
598
  const binary = resolveSharedserver({
548
599
  label: "mcp-combiner",
549
600
  minVersion: SHAREDSERVER_MIN_VERSION,
@@ -620,6 +671,11 @@ export default function mcpCombiner(pi) {
620
671
  }
621
672
  attachment = { binary, name };
622
673
  log("info", `combiner "${name}" attached on port ${port} (${combiner.cmd} ${wrappedArgs.join(" ")})`);
674
+ };
675
+ pi.on("session_start", (_event, ctx) => {
676
+ if (attachment)
677
+ return; // already attached in this process
678
+ attachCombiner(makeLog(ctx, notify));
623
679
  });
624
680
  pi.on("session_shutdown", (event) => {
625
681
  // Only "quit" means the Pi process is actually leaving. reload/new/resume/fork