@code-yeongyu/senpi 2026.10.10-5 → 2026.10.10-6

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 (93) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-6FTH3TK6.js → app-server-command-GJ5NXQSY.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-2UUTN6QT.js → chunk-C7LEZMWS.js} +97 -97
  5. package/dist/bundle/chunks/{chunk-ZSYRWQWG.js → chunk-JMI5OCIJ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-ODYQK5XJ.js → chunk-TALS4X7S.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-2PUAD5TD.js → chunk-VGYIPYOK.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-TIMLYJMK.js → chunk-YV2XQFXG.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-TZVDXVHB.js → chunk-ZYHIRFGT.js} +1 -1
  11. package/dist/bundle/chunks/{cli-main-UYTUBG64.js → cli-main-SZ54SQLA.js} +1 -1
  12. package/dist/bundle/chunks/{help-fast-path-EHRZEBRO.js → help-fast-path-W5W3FYYM.js} +1 -1
  13. package/dist/bundle/chunks/{host-command-TOVYSHYH.js → host-command-YG2FIDPN.js} +1 -1
  14. package/dist/bundle/chunks/{interactive-mode-KJIOCRS7.js → interactive-mode-SCNDQ4R6.js} +1 -1
  15. package/dist/bundle/chunks/{multi-session-host-TEFLJTTA.js → multi-session-host-ZDZMPNP5.js} +4 -3
  16. package/dist/bundle/chunks/{package-manager-cli-QSJSIHUR.js → package-manager-cli-4LMYHHKH.js} +1 -1
  17. package/dist/bundle/chunks/{rotation-stream-C5BM7TMM.js → rotation-stream-E3XKSST7.js} +1 -1
  18. package/dist/bundle/chunks/{rpc-mode-RUKK7VBF.js → rpc-mode-ZFNFREN5.js} +1 -1
  19. package/dist/bundle/chunks/{session-control-endpoint-GZDV2FOZ.js → session-control-endpoint-MVSYBGM3.js} +1 -1
  20. package/dist/bundle/chunks/{session-picker-6D4LC3KR.js → session-picker-TNXWY5QY.js} +1 -1
  21. package/dist/bundle/chunks/session-worker.js +158 -157
  22. package/dist/bundle/cli.js +1 -1
  23. package/dist/bundle/index.js +1 -1
  24. package/dist/bundle/rpc-entry.js +1 -1
  25. package/dist/bundle/runtime-manifest.json +1 -1
  26. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -2
  27. package/dist/core/extensions/builtin/anthropic-subscription/options.js +11 -2
  28. package/dist/core/extensions/builtin/anthropic-subscription/session-reattach.js +4 -1
  29. package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
  30. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +5 -0
  31. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +11 -0
  32. package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -0
  33. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +17 -1
  34. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +20 -12
  35. package/dist/core/extensions/builtin/compaction/lane-policy.js +39 -34
  36. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.d.ts +4 -0
  37. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +41 -0
  38. package/dist/core/extensions/builtin/mcp/auth/legacy-lock.d.ts +3 -0
  39. package/dist/core/extensions/builtin/mcp/auth/legacy-lock.js +39 -0
  40. package/dist/core/extensions/builtin/mcp/auth/legacy-migration.d.ts +14 -0
  41. package/dist/core/extensions/builtin/mcp/auth/legacy-migration.js +91 -0
  42. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +1 -1
  43. package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +1 -0
  44. package/dist/core/extensions/builtin/mcp/auth/token-store.js +19 -90
  45. package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +7 -3
  46. package/dist/core/extensions/builtin/mcp/catalog-cache.js +61 -12
  47. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
  48. package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
  49. package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
  50. package/dist/core/extensions/builtin/mcp/expose/call.d.ts +12 -0
  51. package/dist/core/extensions/builtin/mcp/expose/call.js +60 -0
  52. package/dist/core/extensions/builtin/mcp/expose/proxy.js +16 -3
  53. package/dist/core/extensions/builtin/mcp/expose/register.d.ts +3 -1
  54. package/dist/core/extensions/builtin/mcp/expose/register.js +41 -50
  55. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -0
  56. package/dist/core/extensions/builtin/mcp/expose/session.js +5 -1
  57. package/dist/core/extensions/builtin/mcp/expose/tier-b.js +15 -4
  58. package/dist/core/extensions/builtin/mcp/index.js +5 -26
  59. package/dist/core/extensions/builtin/mcp/invocation.d.ts +42 -0
  60. package/dist/core/extensions/builtin/mcp/invocation.js +118 -0
  61. package/dist/core/extensions/builtin/mcp/service-connection.d.ts +3 -0
  62. package/dist/core/extensions/builtin/mcp/service-connection.js +6 -0
  63. package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -0
  64. package/dist/core/extensions/builtin/mcp/service-register.js +45 -1
  65. package/dist/core/extensions/builtin/mcp/service-tools-changed.js +26 -3
  66. package/dist/core/extensions/builtin/mcp/service-types.d.ts +6 -0
  67. package/dist/core/extensions/builtin/mcp/service.js +83 -49
  68. package/dist/core/extensions/builtin/mcp/shared-connection.js +17 -4
  69. package/dist/core/extensions/builtin/mcp/sharing-policy.js +1 -0
  70. package/dist/core/extensions/builtin/mcp/startup-race.js +47 -9
  71. package/dist/core/extensions/builtin/permission-system/dispatch-metadata.d.ts +8 -0
  72. package/dist/core/extensions/builtin/permission-system/dispatch-metadata.js +23 -0
  73. package/dist/core/extensions/builtin/permission-system/dispatch-policy.d.ts +14 -0
  74. package/dist/core/extensions/builtin/permission-system/dispatch-policy.js +53 -0
  75. package/dist/core/extensions/builtin/permission-system/dispatch.d.ts +33 -0
  76. package/dist/core/extensions/builtin/permission-system/dispatch.js +106 -0
  77. package/dist/core/extensions/builtin/permission-system/index.js +25 -4
  78. package/dist/core/extensions/builtin/permission-system/prompt.d.ts +1 -1
  79. package/dist/core/extensions/builtin/permission-system/prompt.js +3 -1
  80. package/dist/core/extensions/builtin/permission-system/service.d.ts +11 -1
  81. package/dist/core/extensions/builtin/permission-system/service.js +14 -4
  82. package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.d.ts +1 -0
  83. package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.js +24 -5
  84. package/dist/modes/rpc/rpc-client.js +14 -2
  85. package/dist/modes/rpc/rpc-request-deadline.d.ts +8 -0
  86. package/dist/modes/rpc/rpc-request-deadline.js +10 -0
  87. package/dist/modes/rpc/rpc-types.d.ts +7 -4
  88. package/dist/modes/rpc/session-binding.js +15 -1
  89. package/dist/modes/rpc/session-command-router.d.ts +1 -0
  90. package/dist/modes/rpc/session-command-router.js +16 -0
  91. package/docs/providers.md +3 -2
  92. package/docs/rpc.md +12 -6
  93. package/package.json +6 -6
@@ -0,0 +1,41 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolveAuthMode } from "./context.js";
4
+ import { McpTokenStore } from "./token-store.js";
5
+ /** Local snapshot only: never migrates credentials, acquires a lock, or refreshes authentication. */
6
+ export function mcpCredentialIdentity(config, serverName, agentDir, env) {
7
+ const mode = resolveAuthMode(config);
8
+ if (mode === "none")
9
+ return "none";
10
+ if (mode === "bearer") {
11
+ const name = config.bearerTokenEnv;
12
+ if (name === undefined)
13
+ return undefined;
14
+ const token = env?.[name] ?? process.env[name];
15
+ if (token === undefined || token.length === 0)
16
+ return undefined;
17
+ return createHash("sha256")
18
+ .update(JSON.stringify(["mcp-catalog-bearer", token]))
19
+ .digest("hex");
20
+ }
21
+ try {
22
+ const store = new McpTokenStore({ serverName, serverUrl: config.url ?? "", agentDir });
23
+ const value = JSON.parse(readFileSync(store.tokensPath, "utf8"));
24
+ if (typeof value !== "object" || value === null || !("accessToken" in value))
25
+ return undefined;
26
+ if (typeof value.accessToken !== "string" || value.accessToken.length === 0)
27
+ return undefined;
28
+ const refreshToken = "refreshToken" in value ? value.refreshToken : undefined;
29
+ if (refreshToken !== undefined && typeof refreshToken !== "string")
30
+ return undefined;
31
+ return createHash("sha256")
32
+ .update(JSON.stringify(["mcp-catalog-oauth", value.accessToken, refreshToken]))
33
+ .digest("hex");
34
+ }
35
+ catch (error) {
36
+ if (error instanceof Error)
37
+ return undefined;
38
+ throw error;
39
+ }
40
+ }
41
+ //# sourceMappingURL=catalog-identity.js.map
@@ -0,0 +1,3 @@
1
+ /** Shares the existing O_EXCL file with synchronous migrators without blocking the event loop. */
2
+ export declare function acquireLegacyLock(path: string, stale: number): Promise<() => Promise<void>>;
3
+ //# sourceMappingURL=legacy-lock.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { open, rm, stat } from "node:fs/promises";
2
+ import { safeDelay } from "../wrap.js";
3
+ /** Shares the existing O_EXCL file with synchronous migrators without blocking the event loop. */
4
+ export async function acquireLegacyLock(path, stale) {
5
+ const deadline = Date.now() + stale;
6
+ for (;;) {
7
+ try {
8
+ const file = await open(path, "wx", 0o600);
9
+ return async () => {
10
+ try {
11
+ await file.close();
12
+ }
13
+ finally {
14
+ await rm(path, { force: true });
15
+ }
16
+ };
17
+ }
18
+ catch (error) {
19
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
20
+ throw error;
21
+ try {
22
+ const age = Date.now() - (await stat(path)).mtimeMs;
23
+ if (age >= stale) {
24
+ await rm(path, { force: true });
25
+ continue;
26
+ }
27
+ }
28
+ catch (error) {
29
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
30
+ continue;
31
+ throw error;
32
+ }
33
+ if (Date.now() >= deadline)
34
+ throw new Error("Legacy OAuth migration lock acquisition timed out");
35
+ await safeDelay(20);
36
+ }
37
+ }
38
+ }
39
+ //# sourceMappingURL=legacy-lock.js.map
@@ -0,0 +1,14 @@
1
+ export interface LegacyMigration<TRecord> {
2
+ readonly rootDir: string;
3
+ readonly lockPath: string;
4
+ readonly legacyTokens: string;
5
+ readonly tokensPath: string;
6
+ readonly stale: number;
7
+ readonly disableLock: boolean;
8
+ readonly read: (path: string) => TRecord | undefined;
9
+ readonly write: (record: TRecord) => void;
10
+ readonly lockError: (cause: unknown) => Error;
11
+ }
12
+ export declare function adoptLegacyRecord<TRecord>(options: LegacyMigration<TRecord>): TRecord | undefined;
13
+ export declare function readRecordAsync<TRecord>(options: LegacyMigration<TRecord>): Promise<TRecord | undefined>;
14
+ //# sourceMappingURL=legacy-migration.d.ts.map
@@ -0,0 +1,91 @@
1
+ import { closeSync, mkdirSync, openSync, rmSync, statSync } from "node:fs";
2
+ import { acquireLegacyLock } from "./legacy-lock.js";
3
+ export function adoptLegacyRecord(options) {
4
+ const release = acquireSync(options);
5
+ try {
6
+ return claim(options);
7
+ }
8
+ finally {
9
+ release();
10
+ }
11
+ }
12
+ export async function readRecordAsync(options) {
13
+ const record = options.read(options.tokensPath);
14
+ if (record !== undefined)
15
+ return record;
16
+ mkdirSync(options.rootDir, { mode: 0o700, recursive: true });
17
+ let release;
18
+ try {
19
+ release = options.disableLock ? async () => undefined : await acquireLegacyLock(options.lockPath, options.stale);
20
+ }
21
+ catch (cause) {
22
+ throw options.lockError(cause);
23
+ }
24
+ try {
25
+ return claim(options);
26
+ }
27
+ finally {
28
+ await release();
29
+ }
30
+ }
31
+ function claim(options) {
32
+ const legacy = options.read(options.legacyTokens);
33
+ if (legacy === undefined)
34
+ return undefined;
35
+ const existing = options.read(options.tokensPath);
36
+ if (existing !== undefined) {
37
+ rmSync(options.legacyTokens, { force: true });
38
+ return existing;
39
+ }
40
+ options.write(legacy);
41
+ rmSync(options.legacyTokens, { force: true });
42
+ return legacy;
43
+ }
44
+ function acquireSync(options) {
45
+ if (options.disableLock)
46
+ return () => undefined;
47
+ mkdirSync(options.rootDir, { mode: 0o700, recursive: true });
48
+ const deadline = Date.now() + options.stale;
49
+ for (;;) {
50
+ let fd;
51
+ try {
52
+ fd = openSync(options.lockPath, "wx", 0o600);
53
+ const file = fd;
54
+ return () => {
55
+ try {
56
+ closeSync(file);
57
+ }
58
+ catch {
59
+ // A caller may already have closed its descriptor.
60
+ }
61
+ rmSync(options.lockPath, { force: true });
62
+ };
63
+ }
64
+ catch (cause) {
65
+ if (fd !== undefined)
66
+ closeSync(fd);
67
+ if (!(cause instanceof Error) || !("code" in cause) || cause.code !== "EEXIST")
68
+ throw cause;
69
+ let age;
70
+ try {
71
+ age = Date.now() - statSync(options.lockPath).mtimeMs;
72
+ }
73
+ catch (error) {
74
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
75
+ continue;
76
+ throw error;
77
+ }
78
+ if (age >= options.stale) {
79
+ rmSync(options.lockPath, { force: true });
80
+ continue;
81
+ }
82
+ if (Date.now() >= deadline)
83
+ throw options.lockError(new Error(`legacy migration lock held for ${age}ms`));
84
+ const until = Date.now() + 20;
85
+ while (Date.now() < until) {
86
+ // Preserve the synchronous API; background preparation uses readRecordAsync.
87
+ }
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=legacy-migration.js.map
@@ -28,7 +28,7 @@ export class McpRefreshManager {
28
28
  this.#options = options;
29
29
  }
30
30
  async ensureFresh() {
31
- const record = this.#provider.store.read();
31
+ const record = await this.#provider.store.readAsync();
32
32
  if (record?.accessToken === undefined)
33
33
  return undefined;
34
34
  if (!isTokenStale(record, Date.now()))
@@ -37,6 +37,7 @@ export declare class McpTokenStore<TRecord extends McpStoredAuth = McpStoredAuth
37
37
  get lockPath(): string;
38
38
  get legacyDir(): string;
39
39
  read(): TRecord | undefined;
40
+ readAsync(): Promise<TRecord | undefined>;
40
41
  update(mutate: (current: TRecord | undefined) => TRecord | undefined): Promise<TRecord | undefined>;
41
42
  write(record: TRecord): Promise<void>;
42
43
  withLock<T>(fn: () => Promise<T> | T): Promise<T>;
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import lockfile from "proper-lockfile";
5
5
  import { getAgentDir } from "../../../../../config.js";
6
+ import { adoptLegacyRecord, readRecordAsync } from "./legacy-migration.js";
6
7
  const AUTH_ROOT = "mcp-auth";
7
8
  const TOKENS_FILE = "tokens.json";
8
9
  const INDEX_FILE = "index.json";
@@ -57,95 +58,23 @@ export class McpTokenStore {
57
58
  const record = readJsonFile(this.tokensPath);
58
59
  if (record !== undefined)
59
60
  return record;
60
- return this.#adoptLegacyRecord();
61
- }
62
- // The first server that reads a URL-keyed record takes it over; other servers with the same URL sign in again.
63
- // Claiming is serialized on a lock shared by every consumer of the legacy URL (keyed on the
64
- // legacy hash, not the per-server destination hash), so two processes cannot both read the
65
- // URL-keyed record before either removes it and duplicate a rotating grant across identities.
66
- #adoptLegacyRecord() {
67
- const release = this.#acquireLegacyLockSync();
68
- try {
69
- // Recheck under the lock: another process may have already claimed the record.
70
- const legacyTokens = join(this.legacyDir, TOKENS_FILE);
71
- const legacy = readJsonFile(legacyTokens);
72
- if (legacy === undefined)
73
- return undefined;
74
- // Recheck the destination before writing so a delayed migrator cannot
75
- // overwrite a newer same-server record another process already refreshed.
76
- const existing = readJsonFile(this.tokensPath);
77
- if (existing !== undefined) {
78
- rmSync(legacyTokens, { force: true });
79
- return existing;
80
- }
81
- this.#writeAtomic(legacy);
82
- this.#writeIndex();
83
- rmSync(legacyTokens, { force: true });
84
- return legacy;
85
- }
86
- finally {
87
- release();
88
- }
89
- }
90
- // The lock is a file created with O_EXCL (atomic create-or-fail), keyed on the
91
- // legacy hash so every server of the URL contends on the same path. It is only
92
- // ever taken by this migration (never nested under the per-server update lock),
93
- // and release removes a plain file, so it cannot linger the way a directory can.
94
- #acquireLegacyLockSync() {
95
- if (this.#disableLock)
96
- return () => undefined;
97
- mkdirSync(this.rootDir, { mode: 0o700, recursive: true });
98
- const lockFile = this.#legacyLockFile;
99
- const stale = this.#lock.stale;
100
- const deadline = Date.now() + stale;
101
- for (;;) {
102
- let fd;
103
- try {
104
- fd = openSync(lockFile, "wx", 0o600);
105
- const file = fd;
106
- return () => {
107
- try {
108
- closeSync(file);
109
- }
110
- catch {
111
- // already closed
112
- }
113
- rmSync(lockFile, { force: true });
114
- };
115
- }
116
- catch (cause) {
117
- if (fd !== undefined) {
118
- try {
119
- closeSync(fd);
120
- }
121
- catch {
122
- // ignore
123
- }
124
- }
125
- const code = cause.code;
126
- if (code !== "EEXIST")
127
- throw cause;
128
- let age = 0;
129
- try {
130
- age = Date.now() - statSync(lockFile).mtimeMs;
131
- }
132
- catch {
133
- continue; // lock vanished between attempts; retry
134
- }
135
- if (age >= stale) {
136
- rmSync(lockFile, { force: true });
137
- continue;
138
- }
139
- if (Date.now() >= deadline) {
140
- throw new LockAcquireError(lockFile, new Error(`legacy migration lock held for ${age}ms`));
141
- }
142
- // bounded spin; the claim window is tiny (a read + one write + one delete)
143
- const until = Date.now() + 20;
144
- while (Date.now() < until) {
145
- // spin
146
- }
147
- }
148
- }
61
+ return adoptLegacyRecord(this.#migration());
62
+ }
63
+ async readAsync() {
64
+ return readRecordAsync(this.#migration());
65
+ }
66
+ #migration() {
67
+ return {
68
+ rootDir: this.rootDir,
69
+ lockPath: this.#legacyLockFile,
70
+ legacyTokens: join(this.legacyDir, TOKENS_FILE),
71
+ tokensPath: this.tokensPath,
72
+ stale: this.#lock.stale,
73
+ disableLock: this.#disableLock,
74
+ read: readJsonFile,
75
+ write: (record) => this.writeUnlocked(record),
76
+ lockError: (cause) => new LockAcquireError(this.#legacyLockFile, cause),
77
+ };
149
78
  }
150
79
  async update(mutate) {
151
80
  const release = await this.#acquire();
@@ -3,6 +3,7 @@ import type { McpServerConfig } from "./config-schema.ts";
3
3
  import type { ServerConnection } from "./connection.ts";
4
4
  type ListedTool = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
5
5
  type ListedResource = Awaited<ReturnType<Client["listResources"]>>["resources"][number];
6
+ type ListedResourceTemplate = Awaited<ReturnType<Client["listResourceTemplates"]>>["resourceTemplates"][number];
6
7
  type ListedPrompt = Awaited<ReturnType<Client["listPrompts"]>>["prompts"][number];
7
8
  export interface McpCatalogCacheFile {
8
9
  readonly version: 1;
@@ -10,16 +11,19 @@ export interface McpCatalogCacheFile {
10
11
  }
11
12
  export interface McpCachedServerCatalog {
12
13
  readonly configHash: string;
14
+ readonly credentialIdentity?: string;
13
15
  readonly fetchedAt: number;
14
16
  readonly tools: ListedTool[];
15
17
  readonly resources: ListedResource[];
18
+ readonly resourceTemplates?: ListedResourceTemplate[];
16
19
  readonly prompts: ListedPrompt[];
17
20
  readonly instructions?: string;
18
21
  }
19
22
  export declare function getMcpCatalogCachePath(agentDir?: string): string;
20
23
  export declare function readMcpCatalogCache(agentDir?: string): Promise<McpCatalogCacheFile>;
21
- export declare function getValidCachedServer(cache: McpCatalogCacheFile, serverName: string, configHash: string, now?: number): McpCachedServerCatalog | undefined;
22
- export declare function collectServerCatalogForCache(connection: ServerConnection, config: McpServerConfig, configHash: string): Promise<McpCachedServerCatalog>;
23
- export declare function writeMcpCachedServer(agentDir: string | undefined, serverName: string, server: McpCachedServerCatalog): Promise<void>;
24
+ export declare function getValidCachedServer(cache: McpCatalogCacheFile, serverName: string, configHash: string, credentialIdentity?: string): McpCachedServerCatalog | undefined;
25
+ export declare function cachedCatalogNeedsRefresh(cached: McpCachedServerCatalog, now?: number): boolean;
26
+ export declare function collectServerCatalogForCache(connection: ServerConnection, config: McpServerConfig, configHash: string, credentialIdentity?: string): Promise<McpCachedServerCatalog>;
27
+ export declare function writeMcpCachedServer(agentDir: string | undefined, serverName: string, server: McpCachedServerCatalog, isCurrent?: () => boolean): Promise<void>;
24
28
  export {};
25
29
  //# sourceMappingURL=catalog-cache.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
+ import lockfile from "proper-lockfile";
4
5
  import { getAgentDir } from "../../../../config.js";
5
6
  import { collectAllPages } from "./expose/pagination.js";
6
7
  const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -17,33 +18,62 @@ export async function readMcpCatalogCache(agentDir) {
17
18
  return EMPTY_CACHE;
18
19
  }
19
20
  }
20
- export function getValidCachedServer(cache, serverName, configHash, now = Date.now()) {
21
+ export function getValidCachedServer(cache, serverName, configHash, credentialIdentity = "none") {
21
22
  const cached = cache.servers[serverName];
22
23
  if (cached === undefined)
23
24
  return undefined;
24
25
  if (cached.configHash !== configHash)
25
26
  return undefined;
26
- if (now - cached.fetchedAt > CACHE_TTL_MS)
27
+ if (cached.credentialIdentity !== credentialIdentity &&
28
+ !(credentialIdentity === "none" && cached.credentialIdentity === undefined)) {
27
29
  return undefined;
30
+ }
28
31
  return cached;
29
32
  }
30
- export async function collectServerCatalogForCache(connection, config, configHash) {
33
+ export function cachedCatalogNeedsRefresh(cached, now = Date.now()) {
34
+ return now - cached.fetchedAt > CACHE_TTL_MS;
35
+ }
36
+ export async function collectServerCatalogForCache(connection, config, configHash, credentialIdentity) {
31
37
  const tools = await collectAllPages((cursor) => connection.client.listTools(cursor === undefined ? {} : { cursor }, { timeout: config.requestTimeoutMs }));
32
38
  const resources = await collectOptionalPages((cursor) => connection.client.listResources(cursor === undefined ? {} : { cursor }, { timeout: config.requestTimeoutMs }));
33
39
  const prompts = await collectOptionalPages((cursor) => connection.client.listPrompts(cursor === undefined ? {} : { cursor }, { timeout: config.requestTimeoutMs }));
40
+ const resourceTemplates = await collectOptionalPages((cursor) => connection.client.listResourceTemplates(cursor === undefined ? {} : { cursor }, {
41
+ timeout: config.requestTimeoutMs,
42
+ }));
34
43
  return {
35
44
  configHash,
45
+ credentialIdentity,
36
46
  fetchedAt: Date.now(),
37
47
  instructions: connection.client.getInstructions(),
38
48
  prompts,
39
49
  resources,
50
+ resourceTemplates,
40
51
  tools: tools.items,
41
52
  };
42
53
  }
43
- export async function writeMcpCachedServer(agentDir, serverName, server) {
44
- const cache = await readMcpCatalogCache(agentDir);
45
- const next = { version: 1, servers: { ...cache.servers, [serverName]: server } };
46
- await atomicWriteJson(getMcpCatalogCachePath(agentDir), next);
54
+ export async function writeMcpCachedServer(agentDir, serverName, server, isCurrent) {
55
+ if (isCurrent?.() === false)
56
+ return;
57
+ const path = getMcpCatalogCachePath(agentDir);
58
+ await mkdir(dirname(path), { recursive: true });
59
+ const release = await lockfile.lock(dirname(path), {
60
+ lockfilePath: `${path}.lock`,
61
+ realpath: false,
62
+ retries: { retries: 50, factor: 1.2, minTimeout: 20, maxTimeout: 200 },
63
+ stale: 30_000,
64
+ });
65
+ try {
66
+ if (isCurrent?.() === false)
67
+ return;
68
+ const cache = await readMcpCatalogCache(agentDir);
69
+ if (isCurrent?.() === false)
70
+ return;
71
+ const next = { version: 1, servers: { ...cache.servers, [serverName]: server } };
72
+ await atomicWriteJson(path, next, isCurrent);
73
+ }
74
+ finally {
75
+ await release();
76
+ }
47
77
  }
48
78
  async function collectOptionalPages(listFn) {
49
79
  try {
@@ -54,11 +84,18 @@ async function collectOptionalPages(listFn) {
54
84
  return [];
55
85
  }
56
86
  }
57
- async function atomicWriteJson(path, value) {
87
+ async function atomicWriteJson(path, value, isCurrent) {
58
88
  await mkdir(dirname(path), { recursive: true });
59
89
  const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
60
- await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
61
- await rename(tmp, path);
90
+ try {
91
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
92
+ if (isCurrent?.() === false)
93
+ return;
94
+ await rename(tmp, path);
95
+ }
96
+ finally {
97
+ await rm(tmp, { force: true });
98
+ }
62
99
  }
63
100
  function normalizeCacheFile(value) {
64
101
  if (!isRecord(value) || value.version !== 1 || !isRecord(value.servers))
@@ -80,8 +117,20 @@ function normalizeCachedServer(value) {
80
117
  return undefined;
81
118
  const resources = Array.isArray(value.resources) ? value.resources : [];
82
119
  const prompts = Array.isArray(value.prompts) ? value.prompts : [];
120
+ const resourceTemplates = Array.isArray(value.resourceTemplates)
121
+ ? value.resourceTemplates
122
+ : [];
83
123
  const instructions = typeof value.instructions === "string" ? value.instructions : undefined;
84
- return { configHash: value.configHash, fetchedAt: value.fetchedAt, instructions, prompts, resources, tools };
124
+ return {
125
+ configHash: value.configHash,
126
+ credentialIdentity: typeof value.credentialIdentity === "string" ? value.credentialIdentity : undefined,
127
+ fetchedAt: value.fetchedAt,
128
+ instructions,
129
+ prompts,
130
+ resources,
131
+ resourceTemplates,
132
+ tools,
133
+ };
85
134
  }
86
135
  function normalizeTools(value) {
87
136
  if (!Array.isArray(value))
@@ -4,6 +4,7 @@ import type { McpServerConfig, McpSettings } from "./config-schema.ts";
4
4
  import type { ServerConnection } from "./connection.ts";
5
5
  import type { McpOutputArtifacts } from "./guard/output-guard.ts";
6
6
  import type { McpEnsureFreshAuth } from "./health.ts";
7
+ import type { McpInvocationResolver } from "./invocation.ts";
7
8
  type ListedTool = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
8
9
  export interface McpToolCatalogEntry {
9
10
  server: string;
@@ -18,8 +19,9 @@ export interface McpToolCatalogEntry {
18
19
  agentDir?: string;
19
20
  artifacts?: McpOutputArtifacts;
20
21
  outputGuard?: McpSettings["outputGuard"];
22
+ invocation?: McpInvocationResolver;
21
23
  }
22
- type McpToolCatalogOptions = Pick<McpToolCatalogEntry, "agentDir" | "artifacts" | "ensureFresh" | "outputGuard">;
24
+ type McpToolCatalogOptions = Pick<McpToolCatalogEntry, "agentDir" | "artifacts" | "ensureFresh" | "outputGuard" | "invocation">;
23
25
  export declare function collectToolCatalog(server: string, connection: ServerConnection, config: McpServerConfig, options?: McpToolCatalogOptions): Promise<McpToolCatalogEntry[]>;
24
26
  /** What registering these tools with this catalog's resources and prompts exposes;
25
27
  * equal identities register identical definitions. */
@@ -8,6 +8,7 @@ export async function collectToolCatalog(server, connection, config, options = {
8
8
  connection,
9
9
  description: tool.description,
10
10
  ensureFresh: options.ensureFresh,
11
+ invocation: options.invocation,
11
12
  outputGuard: options.outputGuard,
12
13
  requestTimeoutMs: config.requestTimeoutMs,
13
14
  schema: tool.inputSchema,
@@ -33,6 +34,7 @@ export function cachedToolsToCatalogEntries(server, catalog, connection, request
33
34
  description: tool.description,
34
35
  ensureConnected,
35
36
  ensureFresh: options.ensureFresh,
37
+ invocation: options.invocation,
36
38
  outputGuard: options.outputGuard,
37
39
  requestTimeoutMs,
38
40
  schema: tool.inputSchema,
@@ -19,6 +19,8 @@ export type ServerConnectionToolsChangedEvent = {
19
19
  readonly cause: "connect" | "notification";
20
20
  };
21
21
  export interface ServerConnectionOptions {
22
+ readonly credentialIdentity?: string;
23
+ readonly credentialsCurrent?: () => boolean;
22
24
  readonly serverName: string;
23
25
  readonly config: McpServerConfig;
24
26
  readonly logger: McpLogger;
@@ -0,0 +1,12 @@
1
+ import type { McpToolCatalogEntry } from "../catalog.ts";
2
+ import type { McpInvocation, McpInvocationError } from "../invocation.ts";
3
+ import type { McpToolDefinition } from "./register.ts";
4
+ export declare function callMcpTool(entry: McpToolCatalogEntry, args: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: Parameters<McpToolDefinition["execute"]>[3], label: string, invocation: McpInvocation | undefined): Promise<{
5
+ kind: "called";
6
+ entry: McpToolCatalogEntry;
7
+ result: Awaited<ReturnType<McpToolCatalogEntry["connection"]["client"]["callTool"]>>;
8
+ } | {
9
+ kind: "refused";
10
+ error: McpInvocationError;
11
+ }>;
12
+ //# sourceMappingURL=call.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { ToolExecError } from "../errors.js";
2
+ import { ensureMcpToolCallConnection, withMcpRetriableFailedSendRetry, withMcpSessionExpiryRetry } from "../health.js";
3
+ import { runMcpConnectionLifecycleCall } from "../idle.js";
4
+ export async function callMcpTool(entry, args, signal, onUpdate, label, invocation) {
5
+ try {
6
+ return await runMcpConnectionLifecycleCall(entry.connection, () => withMcpSessionExpiryRetry(entry.connection, async () => {
7
+ if (entry.invocation === undefined) {
8
+ await ensureMcpToolCallConnection(entry.connection, entry.ensureFresh);
9
+ await entry.ensureConnected?.();
10
+ }
11
+ const prepare = async () => {
12
+ for (;;) {
13
+ const prepared = await entry.invocation?.resolve(entry, args, invocation, signal);
14
+ if (prepared === undefined || prepared.kind === "refused" || prepared.isCurrent()) {
15
+ return prepared;
16
+ }
17
+ }
18
+ };
19
+ // Readiness and its bounded renewal are not failed remote sends.
20
+ // Only a call attempt below enters the existing failed-send retry.
21
+ let next = await prepare();
22
+ return await withMcpRetriableFailedSendRetry(entry.connection, async () => {
23
+ for (;;) {
24
+ const prepared = next ?? (await prepare());
25
+ next = undefined;
26
+ if (prepared?.kind === "refused")
27
+ return prepared;
28
+ if (prepared !== undefined && !prepared.isCurrent())
29
+ continue;
30
+ const current = prepared?.entry ?? entry;
31
+ const result = await current.connection.client.callTool({ name: current.tool, arguments: args }, undefined, {
32
+ onprogress: (progress) => {
33
+ onUpdate?.({
34
+ content: [{ type: "text", text: formatProgress(label, progress) }],
35
+ details: { progress, server: entry.server, tool: entry.tool },
36
+ });
37
+ },
38
+ signal,
39
+ timeout: current.requestTimeoutMs,
40
+ });
41
+ return { kind: "called", entry: current, result };
42
+ }
43
+ });
44
+ }));
45
+ }
46
+ catch (error) {
47
+ const label = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
48
+ throw new ToolExecError(`ToolExecError: ${label}`, {
49
+ cause: error,
50
+ phase: "call",
51
+ serverName: entry.server,
52
+ });
53
+ }
54
+ }
55
+ function formatProgress(label, progress) {
56
+ const total = progress.total === undefined ? "" : `/${progress.total}`;
57
+ const message = progress.message === undefined ? "" : ` ${progress.message}`;
58
+ return `${label} progress ${progress.progress}${total}${message}`.trim();
59
+ }
60
+ //# sourceMappingURL=call.js.map
@@ -10,6 +10,7 @@
10
10
  // self-correct; unknown tool names get nearest-match suggestions.
11
11
  import { Text } from "@earendil-works/pi-tui";
12
12
  import { Type } from "typebox";
13
+ import { registerDispatchIdentity } from "../../permission-system/dispatch-metadata.js";
13
14
  import { buildBm25Index } from "../../tool-search/engine/bm25.js";
14
15
  import { deriveMcpRegistrationId } from "../../tool-search/engine/marker.js";
15
16
  import { executeMcpCatalogEntry, mapMcpCatalogNames } from "./register.js";
@@ -38,14 +39,21 @@ export function createMcpProxyTool(server, entries) {
38
39
  registrationId: deriveMcpRegistrationId(server, entry.tool),
39
40
  })));
40
41
  const name = named[0]?.name.split("_").slice(0, 2).join("_") ?? `mcp_${server}`;
42
+ const parameters = Type.Object(ParamsSchema.properties);
43
+ registerDispatchIdentity(parameters, (input) => {
44
+ if (input.op !== "call" || typeof input.tool !== "string")
45
+ return undefined;
46
+ const entry = byTool.get(input.tool);
47
+ return entry?.invocation?.identity(entry);
48
+ });
41
49
  return {
42
50
  name,
43
51
  label: `MCP proxy for ${server}`,
44
52
  description: `Gateway to the '${server}' MCP server (${entries.length} tools). Use op:"search" with a query to find tools, op:"describe" with a tool name for its schema, then op:"call" with tool + args (args is a JSON object STRING — no strict validation, so match the described schema exactly).`,
45
53
  promptSnippet: `Proxy for the ${server} MCP server: search -> describe -> call (args as a JSON string).`,
46
- parameters: ParamsSchema,
54
+ parameters,
47
55
  executionMode: "parallel",
48
- async execute(_toolCallId, params, signal, onUpdate) {
56
+ async execute(toolCallId, params, signal, onUpdate, context) {
49
57
  if (params.op === "search") {
50
58
  const matches = index.search(params.query ?? "", 10, {});
51
59
  const body = matches.length === 0
@@ -73,7 +81,12 @@ export function createMcpProxyTool(server, entries) {
73
81
  catch (error) {
74
82
  return textResult(server, `Invalid args for '${entry.tool}': ${error instanceof Error ? error.message : String(error)}. Pass args as a JSON object STRING matching the schema from op:"describe", e.g. {"op":"call","tool":"${entry.tool}","args":"{\\"key\\": \\"value\\"}"}.`);
75
83
  }
76
- return await executeMcpCatalogEntry(entry, args, signal, onUpdate);
84
+ return await executeMcpCatalogEntry(entry, args, signal, onUpdate, {
85
+ toolCallId,
86
+ toolName: name,
87
+ input: params,
88
+ context,
89
+ });
77
90
  },
78
91
  renderCall(args, theme) {
79
92
  const detail = args.op === "search" ? (args.query ?? "") : (args.tool ?? "");