apex-code 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/README.md +7 -8
- package/dist/cli/agent-lifecycle.d.ts +24 -0
- package/dist/cli/agent-lifecycle.d.ts.map +1 -0
- package/dist/cli/agent-lifecycle.js +126 -0
- package/dist/cli/agent-lifecycle.js.map +1 -0
- package/dist/cli/args.d.ts +13 -12
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +80 -30
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -130
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -8
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +160 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +265 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/delegation/agents.d.ts.map +1 -1
- package/dist/core/delegation/agents.js +2 -1
- package/dist/core/delegation/agents.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +674 -1
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +1338 -66
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +3 -4
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +4 -5
- package/dist/core/extensions/source-runtime.d.ts.map +1 -1
- package/dist/core/extensions/source-runtime.js +4 -5
- package/dist/core/extensions/source-runtime.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +2 -1
- package/dist/core/formatter-lifecycle.d.ts.map +1 -1
- package/dist/core/formatter-lifecycle.js +2 -1
- package/dist/core/formatter-lifecycle.js.map +1 -1
- package/dist/core/hooks/types.d.ts +2 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js +2 -3
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/http-idle-timeout.d.ts +3 -5
- package/dist/core/http-idle-timeout.d.ts.map +1 -1
- package/dist/core/http-idle-timeout.js +3 -5
- package/dist/core/http-idle-timeout.js.map +1 -1
- package/dist/core/mcp/config.d.ts +18 -1
- package/dist/core/mcp/config.d.ts.map +1 -1
- package/dist/core/mcp/config.js +24 -7
- package/dist/core/mcp/config.js.map +1 -1
- package/dist/core/mcp/connector.d.ts +2 -3
- package/dist/core/mcp/connector.d.ts.map +1 -1
- package/dist/core/mcp/connector.js.map +1 -1
- package/dist/core/mcp/oauth/authorize.d.ts +2 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
- package/dist/core/mcp/oauth/authorize.js +5 -2
- package/dist/core/mcp/oauth/authorize.js.map +1 -1
- package/dist/core/mcp/oauth/mcp-token.d.ts +4 -6
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -1
- package/dist/core/mcp/oauth/mcp-token.js +5 -7
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -1
- package/dist/core/mcp/runtime.d.ts +1 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -1
- package/dist/core/mcp/runtime.js +2 -1
- package/dist/core/mcp/runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +0 -15
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/store.d.ts +6 -11
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +23 -51
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/project-resources.d.ts +62 -0
- package/dist/core/project-resources.d.ts.map +1 -0
- package/dist/core/project-resources.js +42 -0
- package/dist/core/project-resources.js.map +1 -0
- package/dist/core/sdk.d.ts +62 -12
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +313 -44
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-share.d.ts +6 -8
- package/dist/core/session-share.d.ts.map +1 -1
- package/dist/core/session-share.js +9 -12
- package/dist/core/session-share.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -21
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -12
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +0 -47
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +8 -0
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +33 -3
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/web-fetch.d.ts.map +1 -1
- package/dist/core/tools/web-fetch.js +4 -8
- package/dist/core/tools/web-fetch.js.map +1 -1
- package/dist/core/tools/web-search-exa.d.ts +3 -6
- package/dist/core/tools/web-search-exa.d.ts.map +1 -1
- package/dist/core/tools/web-search-exa.js +5 -8
- package/dist/core/tools/web-search-exa.js.map +1 -1
- package/dist/core/trust-manager.d.ts +6 -5
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +42 -19
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/web-search-provider.d.ts +0 -10
- package/dist/core/web-search-provider.d.ts.map +1 -1
- package/dist/core/web-search-provider.js +2 -16
- package/dist/core/web-search-provider.js.map +1 -1
- package/dist/core/workspace/git-observer.d.ts +16 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -1
- package/dist/core/workspace/git-observer.js +8 -1
- package/dist/core/workspace/git-observer.js.map +1 -1
- package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
- package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
- package/dist/core/workspace/git-worktree-owner.js +240 -0
- package/dist/core/workspace/git-worktree-owner.js.map +1 -0
- package/dist/main.d.ts +0 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/server.d.ts +43 -1
- package/dist/modes/acp/server.d.ts.map +1 -1
- package/dist/modes/acp/server.js +78 -0
- package/dist/modes/acp/server.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +0 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +38 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +110 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +0 -25
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +3 -52
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +0 -2
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +0 -2
- package/dist/utils/version-check.js.map +1 -1
- package/docs/settings.md +2 -7
- package/npm-shrinkwrap.json +5 -5
- package/package.json +2 -2
- package/dist/core/sandbox/bwrap-arguments.d.ts +0 -41
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +0 -1
- package/dist/core/sandbox/bwrap-arguments.js +0 -136
- package/dist/core/sandbox/bwrap-arguments.js.map +0 -1
- package/dist/core/sandbox/child-entry.d.ts +0 -2
- package/dist/core/sandbox/child-entry.d.ts.map +0 -1
- package/dist/core/sandbox/child-entry.js +0 -24
- package/dist/core/sandbox/child-entry.js.map +0 -1
- package/dist/core/sandbox/cli-launch.d.ts +0 -162
- package/dist/core/sandbox/cli-launch.d.ts.map +0 -1
- package/dist/core/sandbox/cli-launch.js +0 -366
- package/dist/core/sandbox/cli-launch.js.map +0 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +0 -31
- package/dist/core/sandbox/cli-supervisor.d.ts.map +0 -1
- package/dist/core/sandbox/cli-supervisor.js +0 -147
- package/dist/core/sandbox/cli-supervisor.js.map +0 -1
- package/dist/core/sandbox/default-hosts.d.ts +0 -28
- package/dist/core/sandbox/default-hosts.d.ts.map +0 -1
- package/dist/core/sandbox/default-hosts.js +0 -69
- package/dist/core/sandbox/default-hosts.js.map +0 -1
- package/dist/core/sandbox/full-access.d.ts +0 -28
- package/dist/core/sandbox/full-access.d.ts.map +0 -1
- package/dist/core/sandbox/full-access.js +0 -63
- package/dist/core/sandbox/full-access.js.map +0 -1
- package/dist/core/sandbox/git-identity.d.ts +0 -57
- package/dist/core/sandbox/git-identity.d.ts.map +0 -1
- package/dist/core/sandbox/git-identity.js +0 -78
- package/dist/core/sandbox/git-identity.js.map +0 -1
- package/dist/core/sandbox/host-approval.d.ts +0 -42
- package/dist/core/sandbox/host-approval.d.ts.map +0 -1
- package/dist/core/sandbox/host-approval.js +0 -99
- package/dist/core/sandbox/host-approval.js.map +0 -1
- package/dist/core/sandbox/linux-backend.d.ts +0 -48
- package/dist/core/sandbox/linux-backend.d.ts.map +0 -1
- package/dist/core/sandbox/linux-backend.js +0 -328
- package/dist/core/sandbox/linux-backend.js.map +0 -1
- package/dist/core/sandbox/macos-backend.d.ts +0 -30
- package/dist/core/sandbox/macos-backend.d.ts.map +0 -1
- package/dist/core/sandbox/macos-backend.js +0 -385
- package/dist/core/sandbox/macos-backend.js.map +0 -1
- package/dist/core/sandbox/network-proxy.d.ts +0 -38
- package/dist/core/sandbox/network-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/network-proxy.js +0 -153
- package/dist/core/sandbox/network-proxy.js.map +0 -1
- package/dist/core/sandbox/network-refusal.d.ts +0 -24
- package/dist/core/sandbox/network-refusal.d.ts.map +0 -1
- package/dist/core/sandbox/network-refusal.js +0 -72
- package/dist/core/sandbox/network-refusal.js.map +0 -1
- package/dist/core/sandbox/policy.d.ts +0 -40
- package/dist/core/sandbox/policy.d.ts.map +0 -1
- package/dist/core/sandbox/policy.js +0 -55
- package/dist/core/sandbox/policy.js.map +0 -1
- package/dist/core/sandbox/profiles.d.ts +0 -25
- package/dist/core/sandbox/profiles.d.ts.map +0 -1
- package/dist/core/sandbox/profiles.js +0 -23
- package/dist/core/sandbox/profiles.js.map +0 -1
- package/dist/core/sandbox/rpc/command-client.d.ts +0 -28
- package/dist/core/sandbox/rpc/command-client.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/command-client.js +0 -86
- package/dist/core/sandbox/rpc/command-client.js.map +0 -1
- package/dist/core/sandbox/rpc/command-proxy.d.ts +0 -48
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/command-proxy.js +0 -120
- package/dist/core/sandbox/rpc/command-proxy.js.map +0 -1
- package/dist/core/sandbox/rpc/credential-client.d.ts +0 -20
- package/dist/core/sandbox/rpc/credential-client.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/credential-client.js +0 -170
- package/dist/core/sandbox/rpc/credential-client.js.map +0 -1
- package/dist/core/sandbox/rpc/credential-proxy.d.ts +0 -44
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +0 -287
- package/dist/core/sandbox/rpc/credential-proxy.js.map +0 -1
- package/dist/core/sandbox/rpc/framing.d.ts +0 -35
- package/dist/core/sandbox/rpc/framing.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/framing.js +0 -114
- package/dist/core/sandbox/rpc/framing.js.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +0 -47
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-helper.js +0 -222
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +0 -66
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-proxy.js +0 -209
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +0 -1
- package/dist/core/sandbox/supervisor-temp.d.ts +0 -18
- package/dist/core/sandbox/supervisor-temp.d.ts.map +0 -1
- package/dist/core/sandbox/supervisor-temp.js +0 -21
- package/dist/core/sandbox/supervisor-temp.js.map +0 -1
- package/dist/core/sandbox/supervisor.d.ts +0 -62
- package/dist/core/sandbox/supervisor.d.ts.map +0 -1
- package/dist/core/sandbox/supervisor.js +0 -32
- package/dist/core/sandbox/supervisor.js.map +0 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +0 -68
- package/dist/core/sandbox/terminal-handoff.d.ts.map +0 -1
- package/dist/core/sandbox/terminal-handoff.js +0 -221
- package/dist/core/sandbox/terminal-handoff.js.map +0 -1
- package/dist/core/sandbox/terminal-size.d.ts +0 -53
- package/dist/core/sandbox/terminal-size.d.ts.map +0 -1
- package/dist/core/sandbox/terminal-size.js +0 -126
- package/dist/core/sandbox/terminal-size.js.map +0 -1
- package/dist/core/sandbox/violations.d.ts +0 -23
- package/dist/core/sandbox/violations.d.ts.map +0 -1
- package/dist/core/sandbox/violations.js +0 -30
- package/dist/core/sandbox/violations.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,gBAAgB,8FAMnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,SAAS,CAAC;IAChB,8HAA8H;IAC9H,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,8GAA8G;IAC9G,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0JAA0J;IAC1J,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,GAAG,qBAAqB,CAAC;AAEjF,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnH,MAAM,WAAW,WAAW;IAC3B,uDAAuD;IACvD,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACzD;AAED,kHAAkH;AAClH,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC3C,qFAAqF;IACrF,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,WAAW,EAAE,CAAC;IAC1D;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACpG;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,0FAA0F;AAC1F,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAC9C,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,QAAS,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it, which is what makes\n * trust-gated project-scope hooks safe to load from a project at all.\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
|
package/dist/core/hooks/types.js
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The decision vocabulary is restriction-only: `block` short-circuits, `ask`
|
|
8
8
|
* defers to the permission gate, and `allow` is recorded but never bypasses
|
|
9
|
-
* the gate. A hook can narrow what runs, never widen it
|
|
10
|
-
* trust-gated project-scope hooks safe
|
|
11
|
-
* (ADR 0016).
|
|
9
|
+
* the gate. A hook can narrow what runs, never widen it, which is what makes
|
|
10
|
+
* trust-gated project-scope hooks safe to load from a project at all.
|
|
12
11
|
*/
|
|
13
12
|
export const HOOK_EVENT_NAMES = [
|
|
14
13
|
"tool_call",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,WAAW;IACX,aAAa;IACb,eAAe;IACf,UAAU;IACV,wBAAwB;CACf,CAAC;AAsEX,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAC9C,uCAAuC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it, which is what makes\n * trust-gated project-scope hooks safe to load from a project at all.\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on
|
|
3
|
-
* purpose: the dispatcher module loads `undici`, and `settings-manager`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Keeping them here means parsing a settings file never pays for an HTTP stack, while
|
|
7
|
-
* the dispatcher remains the single place that consumes the timeout.
|
|
3
|
+
* purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these
|
|
4
|
+
* two definitions. Keeping them here means parsing a settings file never pays for an HTTP
|
|
5
|
+
* stack, while the dispatcher remains the single place that consumes the timeout.
|
|
8
6
|
*/
|
|
9
7
|
/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */
|
|
10
8
|
export declare const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300000;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-idle-timeout.d.ts","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http-idle-timeout.d.ts","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,uFAAuF;AACvF,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgBzE","sourcesContent":["/**\n * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on\n * purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these\n * two definitions. Keeping them here means parsing a settings file never pays for an HTTP\n * stack, while the dispatcher remains the single place that consumes the timeout.\n */\n\n/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n"]}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on
|
|
3
|
-
* purpose: the dispatcher module loads `undici`, and `settings-manager`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Keeping them here means parsing a settings file never pays for an HTTP stack, while
|
|
7
|
-
* the dispatcher remains the single place that consumes the timeout.
|
|
3
|
+
* purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these
|
|
4
|
+
* two definitions. Keeping them here means parsing a settings file never pays for an HTTP
|
|
5
|
+
* stack, while the dispatcher remains the single place that consumes the timeout.
|
|
8
6
|
*/
|
|
9
7
|
/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */
|
|
10
8
|
export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-idle-timeout.js","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http-idle-timeout.js","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,uFAAuF;AACvF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAsB;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;YAC1C,OAAO,CAAC,CAAC;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAAA,CACzB","sourcesContent":["/**\n * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on\n * purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these\n * two definitions. Keeping them here means parsing a settings file never pays for an HTTP\n * stack, while the dispatcher remains the single place that consumes the timeout.\n */\n\n/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n"]}
|
|
@@ -8,7 +8,24 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type McpConfig } from "./types.ts";
|
|
10
10
|
export declare const PROJECT_CONFIG_FILENAME = ".mcp.json";
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.
|
|
13
|
+
*
|
|
14
|
+
* Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory
|
|
15
|
+
* isolates MCP config the way it already isolates settings, auth, and sessions. Reading
|
|
16
|
+
* the home directory directly made this the one user-scope resolver that ignored the
|
|
17
|
+
* injection seam, which is why suites asserting "no server is configured" picked up the
|
|
18
|
+
* developer's real servers and failed. For the default agent directory the resolved path
|
|
19
|
+
* is unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export declare function globalMcpConfigPath(agentDir?: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Both sources are explicit, and an omitted one means "no config from that scope" rather
|
|
24
|
+
* than "go and find the real one". Defaulting `globalPath` to the user's home made every
|
|
25
|
+
* caller that forgot it read the developer's own servers, which is how suites asserting
|
|
26
|
+
* an empty registry started failing on a configured machine. A boundary resolves the
|
|
27
|
+
* path; this does not reach for one.
|
|
28
|
+
*/
|
|
12
29
|
export declare function loadMcpConfig(options?: {
|
|
13
30
|
projectPath?: string;
|
|
14
31
|
globalPath?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAGN,KAAK,SAAS,EAKd,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE5E;AAgJD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAapG","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\n/**\n * The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.\n *\n * Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory\n * isolates MCP config the way it already isolates settings, auth, and sessions. Reading\n * the home directory directly made this the one user-scope resolver that ignored the\n * injection seam, which is why suites asserting \"no server is configured\" picked up the\n * developer's real servers and failed. For the default agent directory the resolved path\n * is unchanged.\n */\nexport function globalMcpConfigPath(agentDir: string = getAgentDir()): string {\n\treturn join(dirname(agentDir), \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\n/**\n * Both sources are explicit, and an omitted one means \"no config from that scope\" rather\n * than \"go and find the real one\". Defaulting `globalPath` to the user's home made every\n * caller that forgot it read the developer's own servers, which is how suites asserting\n * an empty registry started failing on a configured machine. A boundary resolves the\n * path; this does not reach for one.\n */\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\n\tconst servers = options.globalPath\n\t\t? readServers(options.globalPath, diagnostics)\n\t\t: new Map<string, McpServerConfig>();\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
|
package/dist/core/mcp/config.js
CHANGED
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
* first thing a user hand-edits, so a parse error must never take the session down.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { CONFIG_DIR_NAME } from "../../config.js";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { getAgentDir } from "../../config.js";
|
|
13
12
|
import { ALL_CAPABILITIES } from "../tools/contract.js";
|
|
14
13
|
import { DEFAULT_IDLE_TIMEOUT_MINUTES, DEFAULT_LIFECYCLE, } from "./types.js";
|
|
15
14
|
const LIFECYCLES = ["lazy", "eager", "keep-alive", "lazy-keep-alive"];
|
|
16
15
|
export const PROJECT_CONFIG_FILENAME = ".mcp.json";
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.
|
|
18
|
+
*
|
|
19
|
+
* Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory
|
|
20
|
+
* isolates MCP config the way it already isolates settings, auth, and sessions. Reading
|
|
21
|
+
* the home directory directly made this the one user-scope resolver that ignored the
|
|
22
|
+
* injection seam, which is why suites asserting "no server is configured" picked up the
|
|
23
|
+
* developer's real servers and failed. For the default agent directory the resolved path
|
|
24
|
+
* is unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export function globalMcpConfigPath(agentDir = getAgentDir()) {
|
|
27
|
+
return join(dirname(agentDir), "mcp.json");
|
|
19
28
|
}
|
|
20
29
|
function isRecord(value) {
|
|
21
30
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -152,10 +161,18 @@ function readServers(path, diagnostics) {
|
|
|
152
161
|
}
|
|
153
162
|
return servers;
|
|
154
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Both sources are explicit, and an omitted one means "no config from that scope" rather
|
|
166
|
+
* than "go and find the real one". Defaulting `globalPath` to the user's home made every
|
|
167
|
+
* caller that forgot it read the developer's own servers, which is how suites asserting
|
|
168
|
+
* an empty registry started failing on a configured machine. A boundary resolves the
|
|
169
|
+
* path; this does not reach for one.
|
|
170
|
+
*/
|
|
155
171
|
export function loadMcpConfig(options = {}) {
|
|
156
172
|
const diagnostics = [];
|
|
157
|
-
const
|
|
158
|
-
|
|
173
|
+
const servers = options.globalPath
|
|
174
|
+
? readServers(options.globalPath, diagnostics)
|
|
175
|
+
: new Map();
|
|
159
176
|
if (options.projectPath) {
|
|
160
177
|
for (const [name, config] of readServers(options.projectPath, diagnostics)) {
|
|
161
178
|
servers.set(name, config);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAmB,MAAM,sBAAsB,CAAC;AACzE,OAAO,EACN,4BAA4B,EAC5B,iBAAiB,GAMjB,MAAM,YAAY,CAAC;AAEpB,MAAM,UAAU,GAA4B,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,CAAC,CAAC;AAE/F,MAAM,CAAC,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAEnD,MAAM,UAAU,mBAAmB,GAAW;IAC7C,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;AAAA,CACpD;AAED,SAAS,QAAQ,CAAC,KAAc,EAAoC;IACnE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED,SAAS,YAAY,CAAC,KAAc,EAA0B;IAC7D,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AAAA,CACX;AAED,SAAS,WAAW,CAAC,KAAc,EAAY;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACpG;AAED,SAAS,cAAc,CAAC,KAA8B,EAAyB;IAC9E,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9E,MAAM,GAAG,GAAG,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,IAAI,OAAO,IAAI,GAAG;QAAE,OAAO,wDAAwD,CAAC;IACpF,IAAI,OAAO,EAAE,CAAC;QACb,OAAO;YACN,IAAI,EAAE,OAAO;YACb,OAAO;YACP,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC;YAC7B,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;YAC5B,GAAG,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;SAC1D,CAAC;IACH,CAAC;IACD,IAAI,GAAG,EAAE,CAAC;QACT,OAAO;YACN,IAAI,EAAE,MAAM;YACZ,GAAG;YACH,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC;YACpC,cAAc,EAAE,OAAO,KAAK,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS;SAC3F,CAAC;IACH,CAAC;IACD,OAAO,kCAAkC,CAAC;AAAA,CAC1C;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,KAA8B,EAAE,MAAiC,EAA2B;IACtH,IAAI,CAAC,CAAC,cAAc,IAAI,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACxD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;QACxC,MAAM,CAAC,uEAAuE,CAAC,CAAC;QAChF,OAAO,gBAAgB,CAAC;IACzB,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAc,CAAC;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QAC5C,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,SAAuB,CAAC,EAAE,CAAC;YACpF,QAAQ,CAAC,GAAG,CAAC,SAAuB,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,iCAAiC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,SAAS,cAAc,CAAC,KAA8B,EAAE,MAAiC,EAAgB;IACxG,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,iBAAiB,CAAC;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAqB,CAAC;QAAE,OAAO,KAAqB,CAAC;IAC1G,MAAM,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,iBAAiB,EAAE,CAAC,CAAC;IACjF,OAAO,iBAAiB,CAAC;AAAA,CACzB;AAED,SAAS,gBAAgB,CAAC,KAA8B,EAAU;IACjE,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC;IAChC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,4BAA4B,CAAC;AAAA,CAChH;AAED,SAAS,SAAS,CACjB,KAA8B,EAC9B,SAAuB,EACvB,MAAiC,EACX;IACtB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC;IACzB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC/B,MAAM,CAAC,kDAAkD,CAAC,CAAC;QAC3D,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACtC,MAAM,CAAC,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACrF,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,WAAkC,EAAgC;IACpG,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IACnD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC;IAEtC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,WAAW,CAAC,IAAI,CAAC;YAChB,IAAI;YACJ,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,gCAAgC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;SACjG,CAAC,CAAC;QACH,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;QACnF,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,wBAAwB,EAAE,CAAC,CAAC;YAC5E,SAAS;QACV,CAAC;QACD,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YACnC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;YAC7D,SAAS;QACV,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACtF,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE;YACjB,IAAI;YACJ,SAAS;YACT,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC;YACvC,YAAY,EAAE,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC;YACtC,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC;SACzC,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC;AAAA,CACf;AAED,MAAM,UAAU,aAAa,CAAC,OAAO,GAAkD,EAAE,EAAa;IACrG,MAAM,WAAW,GAA0B,EAAE,CAAC;IAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,EAAE,CAAC;IAE/D,MAAM,OAAO,GAAG,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC;YAC5E,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AAAA,CAChC","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\nexport function globalMcpConfigPath(): string {\n\treturn join(homedir(), CONFIG_DIR_NAME, \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\tconst globalPath = options.globalPath ?? globalMcpConfigPath();\n\n\tconst servers = readServers(globalPath, diagnostics);\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAmB,MAAM,sBAAsB,CAAC;AACzE,OAAO,EACN,4BAA4B,EAC5B,iBAAiB,GAMjB,MAAM,YAAY,CAAC;AAEpB,MAAM,UAAU,GAA4B,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,CAAC,CAAC;AAE/F,MAAM,CAAC,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAQ,GAAW,WAAW,EAAE,EAAU;IAC7E,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAC;AAAA,CAC3C;AAED,SAAS,QAAQ,CAAC,KAAc,EAAoC;IACnE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED,SAAS,YAAY,CAAC,KAAc,EAA0B;IAC7D,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AAAA,CACX;AAED,SAAS,WAAW,CAAC,KAAc,EAAY;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACpG;AAED,SAAS,cAAc,CAAC,KAA8B,EAAyB;IAC9E,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9E,MAAM,GAAG,GAAG,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,IAAI,OAAO,IAAI,GAAG;QAAE,OAAO,wDAAwD,CAAC;IACpF,IAAI,OAAO,EAAE,CAAC;QACb,OAAO;YACN,IAAI,EAAE,OAAO;YACb,OAAO;YACP,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC;YAC7B,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;YAC5B,GAAG,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;SAC1D,CAAC;IACH,CAAC;IACD,IAAI,GAAG,EAAE,CAAC;QACT,OAAO;YACN,IAAI,EAAE,MAAM;YACZ,GAAG;YACH,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC;YACpC,cAAc,EAAE,OAAO,KAAK,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS;SAC3F,CAAC;IACH,CAAC;IACD,OAAO,kCAAkC,CAAC;AAAA,CAC1C;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,KAA8B,EAAE,MAAiC,EAA2B;IACtH,IAAI,CAAC,CAAC,cAAc,IAAI,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACxD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;QACxC,MAAM,CAAC,uEAAuE,CAAC,CAAC;QAChF,OAAO,gBAAgB,CAAC;IACzB,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAc,CAAC;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QAC5C,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,SAAuB,CAAC,EAAE,CAAC;YACpF,QAAQ,CAAC,GAAG,CAAC,SAAuB,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,iCAAiC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,SAAS,cAAc,CAAC,KAA8B,EAAE,MAAiC,EAAgB;IACxG,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,iBAAiB,CAAC;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAqB,CAAC;QAAE,OAAO,KAAqB,CAAC;IAC1G,MAAM,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,iBAAiB,EAAE,CAAC,CAAC;IACjF,OAAO,iBAAiB,CAAC;AAAA,CACzB;AAED,SAAS,gBAAgB,CAAC,KAA8B,EAAU;IACjE,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC;IAChC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,4BAA4B,CAAC;AAAA,CAChH;AAED,SAAS,SAAS,CACjB,KAA8B,EAC9B,SAAuB,EACvB,MAAiC,EACX;IACtB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC;IACzB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC/B,MAAM,CAAC,kDAAkD,CAAC,CAAC;QAC3D,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACtC,MAAM,CAAC,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACrF,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,WAAkC,EAAgC;IACpG,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IACnD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC;IAEtC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,WAAW,CAAC,IAAI,CAAC;YAChB,IAAI;YACJ,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,gCAAgC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;SACjG,CAAC,CAAC;QACH,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;QACnF,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,wBAAwB,EAAE,CAAC,CAAC;YAC5E,SAAS;QACV,CAAC;QACD,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YACnC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;YAC7D,SAAS;QACV,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACtF,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE;YACjB,IAAI;YACJ,SAAS;YACT,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC;YACvC,YAAY,EAAE,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC;YACtC,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC;SACzC,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC;AAAA,CACf;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,OAAO,GAAkD,EAAE,EAAa;IACrG,MAAM,WAAW,GAA0B,EAAE,CAAC;IAE9C,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU;QACjC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC;QAC9C,CAAC,CAAC,IAAI,GAAG,EAA2B,CAAC;IACtC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC;YAC5E,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AAAA,CAChC","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\n/**\n * The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.\n *\n * Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory\n * isolates MCP config the way it already isolates settings, auth, and sessions. Reading\n * the home directory directly made this the one user-scope resolver that ignored the\n * injection seam, which is why suites asserting \"no server is configured\" picked up the\n * developer's real servers and failed. For the default agent directory the resolved path\n * is unchanged.\n */\nexport function globalMcpConfigPath(agentDir: string = getAgentDir()): string {\n\treturn join(dirname(agentDir), \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\n/**\n * Both sources are explicit, and an omitted one means \"no config from that scope\" rather\n * than \"go and find the real one\". Defaulting `globalPath` to the user's home made every\n * caller that forgot it read the developer's own servers, which is how suites asserting\n * an empty registry started failing on a configured machine. A boundary resolves the\n * path; this does not reach for one.\n */\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\n\tconst servers = options.globalPath\n\t\t? readServers(options.globalPath, diagnostics)\n\t\t: new Map<string, McpServerConfig>();\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
|
|
@@ -17,9 +17,8 @@ export declare function resolveAuthorizationHeaders(server: McpServerConfig, oau
|
|
|
17
17
|
export declare function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection>;
|
|
18
18
|
export interface SessionMcpConnectorOptions {
|
|
19
19
|
/**
|
|
20
|
-
* The session's credential store
|
|
21
|
-
*
|
|
22
|
-
* channel). OAuth-configured servers without one fail closed.
|
|
20
|
+
* The session's credential store, `AuthStorage` by default. OAuth-configured
|
|
21
|
+
* servers without one fail closed.
|
|
23
22
|
*/
|
|
24
23
|
credentials?: CredentialStore;
|
|
25
24
|
now?: () => number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAA6B,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1G;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,MAAM,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,MAAM,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAwCD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBnH;AAED,MAAM,WAAW,0BAA0B;IAC1C
|
|
1
|
+
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAA6B,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1G;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,MAAM,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,MAAM,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAwCD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBnH;AAED,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,YAAY,CAchG","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store, `AuthStorage` by default. OAuth-configured\n\t * servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store, `AuthStorage` by default. OAuth-configured\n\t * servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
|
|
@@ -10,6 +10,8 @@ export interface AuthorizeServerOptions {
|
|
|
10
10
|
serverName: string;
|
|
11
11
|
/** Project root for `.mcp.json` discovery. Default: process.cwd(). */
|
|
12
12
|
cwd?: string;
|
|
13
|
+
/** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */
|
|
14
|
+
agentDir?: string;
|
|
13
15
|
/** Credential sink. Default: the host store (`AuthStorage.create()`). */
|
|
14
16
|
credentials?: CredentialStore;
|
|
15
17
|
openBrowser?: (url: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,eAAe,CAAC;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,eAAe,CAAC;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B9F","sourcesContent":["/**\n * Shared entry point for `apex-code mcp auth <server>` and the TUI `/mcp auth`\n * command. Resolves the named server from the merged MCP config, checks it is an\n * OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the\n * line printer and the browser opener.\n */\n\nimport { join } from \"node:path\";\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"../config.ts\";\nimport { runMcpOAuthFlow } from \"./flow.ts\";\n\nexport interface AuthorizeServerOptions {\n\tserverName: string;\n\t/** Project root for `.mcp.json` discovery. Default: process.cwd(). */\n\tcwd?: string;\n\t/** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */\n\tagentDir?: string;\n\t/** Credential sink. Default: the host store (`AuthStorage.create()`). */\n\tcredentials?: CredentialStore;\n\topenBrowser?: (url: string) => void;\n\t/** Injectable flow, for tests. */\n\tflow?: typeof runMcpOAuthFlow;\n\tlog: (line: string) => void;\n}\n\nexport async function authorizeConfiguredServer(options: AuthorizeServerOptions): Promise<void> {\n\tconst { servers } = loadMcpConfig({\n\t\tprojectPath: join(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tconst server = servers.get(options.serverName);\n\tif (!server) {\n\t\tconst names = [...servers.keys()];\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured. Configured servers: ${names.join(\", \") || \"none\"}. Add it to .mcp.json first.`,\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new Error(`MCP server \"${options.serverName}\" is a stdio server; OAuth applies to HTTP servers only.`);\n\t}\n\tif (server.auth !== \"oauth\") {\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured for OAuth. Add \"auth\": \"oauth\" to its entry.`,\n\t\t);\n\t}\n\tconst result = await (options.flow ?? runMcpOAuthFlow)({\n\t\tserverName: options.serverName,\n\t\tserverUrl: server.transport.url,\n\t\tcredentials: options.credentials ?? AuthStorage.create(),\n\t\tdeps: { print: options.log, openBrowser: options.openBrowser ?? (() => {}) },\n\t});\n\toptions.log(\n\t\t`Authorized \"${options.serverName}\". Token stored; expires ${new Date(result.expiresAt).toISOString()}.`,\n\t);\n}\n"]}
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { AuthStorage } from "../../auth-storage.js";
|
|
9
|
-
import { loadMcpConfig, PROJECT_CONFIG_FILENAME } from "../config.js";
|
|
9
|
+
import { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from "../config.js";
|
|
10
10
|
import { runMcpOAuthFlow } from "./flow.js";
|
|
11
11
|
export async function authorizeConfiguredServer(options) {
|
|
12
|
-
const { servers } = loadMcpConfig({
|
|
12
|
+
const { servers } = loadMcpConfig({
|
|
13
|
+
projectPath: join(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),
|
|
14
|
+
globalPath: globalMcpConfigPath(options.agentDir),
|
|
15
|
+
});
|
|
13
16
|
const server = servers.get(options.serverName);
|
|
14
17
|
if (!server) {
|
|
15
18
|
const names = [...servers.keys()];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAgB5C,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,OAA+B,EAAiB;IAC/F,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC;QACjC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,uBAAuB,CAAC;QACxE,UAAU,EAAE,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC;KACjD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAClC,MAAM,IAAI,KAAK,CACd,eAAe,OAAO,CAAC,UAAU,4CAA4C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,8BAA8B,CACrI,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,eAAe,OAAO,CAAC,UAAU,0DAA0D,CAAC,CAAC;IAC9G,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACd,eAAe,OAAO,CAAC,UAAU,kEAAkE,CACnG,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,eAAe,CAAC,CAAC;QACtD,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG;QAC/B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,WAAW,CAAC,MAAM,EAAE;QACxD,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,EAAE;KAC5E,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CACV,eAAe,OAAO,CAAC,UAAU,4BAA4B,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,GAAG,CACxG,CAAC;AAAA,CACF","sourcesContent":["/**\n * Shared entry point for `apex-code mcp auth <server>` and the TUI `/mcp auth`\n * command. Resolves the named server from the merged MCP config, checks it is an\n * OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the\n * line printer and the browser opener.\n */\n\nimport { join } from \"node:path\";\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"../config.ts\";\nimport { runMcpOAuthFlow } from \"./flow.ts\";\n\nexport interface AuthorizeServerOptions {\n\tserverName: string;\n\t/** Project root for `.mcp.json` discovery. Default: process.cwd(). */\n\tcwd?: string;\n\t/** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */\n\tagentDir?: string;\n\t/** Credential sink. Default: the host store (`AuthStorage.create()`). */\n\tcredentials?: CredentialStore;\n\topenBrowser?: (url: string) => void;\n\t/** Injectable flow, for tests. */\n\tflow?: typeof runMcpOAuthFlow;\n\tlog: (line: string) => void;\n}\n\nexport async function authorizeConfiguredServer(options: AuthorizeServerOptions): Promise<void> {\n\tconst { servers } = loadMcpConfig({\n\t\tprojectPath: join(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tconst server = servers.get(options.serverName);\n\tif (!server) {\n\t\tconst names = [...servers.keys()];\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured. Configured servers: ${names.join(\", \") || \"none\"}. Add it to .mcp.json first.`,\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new Error(`MCP server \"${options.serverName}\" is a stdio server; OAuth applies to HTTP servers only.`);\n\t}\n\tif (server.auth !== \"oauth\") {\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured for OAuth. Add \"auth\": \"oauth\" to its entry.`,\n\t\t);\n\t}\n\tconst result = await (options.flow ?? runMcpOAuthFlow)({\n\t\tserverName: options.serverName,\n\t\tserverUrl: server.transport.url,\n\t\tcredentials: options.credentials ?? AuthStorage.create(),\n\t\tdeps: { print: options.log, openBrowser: options.openBrowser ?? (() => {}) },\n\t});\n\toptions.log(\n\t\t`Authorized \"${options.serverName}\". Token stored; expires ${new Date(result.expiresAt).toISOString()}.`,\n\t);\n}\n"]}
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
* The MCP credential seam: how an MCP server's OAuth token is keyed, read, and
|
|
3
3
|
* refreshed through the session's existing `CredentialStore`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* store whose `modify` throws, which is the fail-closed path: refresh never
|
|
10
|
-
* silently degrades to "pretend it worked".
|
|
5
|
+
* That store is `AuthStorage` (direct, lock-serialized writes) unless an embedder
|
|
6
|
+
* supplies its own. A session with neither has a read-only store whose `modify`
|
|
7
|
+
* throws, which is the fail-closed path: refresh never silently degrades to
|
|
8
|
+
* "pretend it worked".
|
|
11
9
|
*/
|
|
12
10
|
import type { CredentialStore } from "@earendil-works/pi-ai";
|
|
13
11
|
import type { McpServerConfig } from "../types.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-token.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/mcp-token.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mcp-token.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/mcp-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAA+B,KAAK,SAAS,EAAqB,MAAM,eAAe,CAAC;AAW/F,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,0FAA0F;AAC1F,UAAU,cAAc;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,qBAAa,qBAAsB,SAAQ,KAAK;CAAG;AAEnD,qBAAa,oBAAqB,SAAQ,KAAK;IAC9C,6FAA6F;IAC7F,KAAK,EAAE,SAAS,GAAG,aAAa,CAAC;IAEjC,YAAY,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,EAG5D;CACD;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,CAItE;AAqBD,wBAAsB,eAAe,CACpC,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAGxC;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACrD,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,EAAE,eAAe,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CAClB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8ElB","sourcesContent":["/**\n * The MCP credential seam: how an MCP server's OAuth token is keyed, read, and\n * refreshed through the session's existing `CredentialStore`.\n *\n * That store is `AuthStorage` (direct, lock-serialized writes) unless an embedder\n * supplies its own. A session with neither has a read-only store whose `modify`\n * throws, which is the fail-closed path: refresh never silently degrades to\n * \"pretend it worked\".\n */\n\nimport type { Credential, CredentialStore } from \"@earendil-works/pi-ai\";\nimport type { McpServerConfig } from \"../types.ts\";\nimport { discoverAuthorizationServer, type FetchLike, oauthErrorMessage } from \"./discover.ts\";\nimport {\n\trefreshAccessToken,\n\tTokenEndpointError,\n\tTokenEndpointUnreachableError,\n\ttype TokenResponse,\n} from \"./token-client.ts\";\n\n/** Refresh this long before expiry so an in-flight call never rides a dying token. */\nconst REFRESH_SKEW_MS = 60_000;\n\nexport function mcpCredentialKey(serverName: string): string {\n\treturn `mcp/${serverName}`;\n}\n\n/** Extra fields carried on the `OAuthCredential` index signature, written by the flow. */\ninterface McpTokenExtras {\n\ttokenEndpoint?: string;\n\tresource?: string;\n\tclientId?: string;\n}\n\nexport interface StoredServerToken {\n\taccessToken: string;\n\t/** Empty when the server issued no refresh token; refresh is impossible then. */\n\trefreshToken: string;\n\texpiresAt: number;\n\textras: McpTokenExtras;\n}\n\nexport class McpOAuthRequiredError extends Error {}\n\nexport class McpOAuthRefreshError extends Error {\n\t/** \"refused\": the server said no; \"unavailable\": the path to the store or network failed. */\n\tstage: \"refused\" | \"unavailable\";\n\n\tconstructor(message: string, stage: \"refused\" | \"unavailable\") {\n\t\tsuper(message);\n\t\tthis.stage = stage;\n\t}\n}\n\nexport function authRequired(serverName: string): McpOAuthRequiredError {\n\treturn new McpOAuthRequiredError(\n\t\t`MCP server \"${serverName}\" requires OAuth and has no stored authorization. Run \\`apex-code mcp auth ${serverName}\\` to authorize it, then retry.`,\n\t);\n}\n\nfunction extrasOf(credential: Credential): McpTokenExtras {\n\tif (credential.type !== \"oauth\") return {};\n\tconst extras: McpTokenExtras = {};\n\tif (typeof credential.tokenEndpoint === \"string\") extras.tokenEndpoint = credential.tokenEndpoint;\n\tif (typeof credential.resource === \"string\") extras.resource = credential.resource;\n\tif (typeof credential.clientId === \"string\") extras.clientId = credential.clientId;\n\treturn extras;\n}\n\nfunction toStoredToken(credential: Credential): StoredServerToken | undefined {\n\tif (credential.type !== \"oauth\") return undefined;\n\treturn {\n\t\taccessToken: credential.access,\n\t\trefreshToken: credential.refresh,\n\t\texpiresAt: credential.expires,\n\t\textras: extrasOf(credential),\n\t};\n}\n\nexport async function readServerToken(\n\tcredentials: CredentialStore,\n\tserverName: string,\n): Promise<StoredServerToken | undefined> {\n\tconst credential: Credential | undefined = await credentials.read(mcpCredentialKey(serverName));\n\treturn credential ? toStoredToken(credential) : undefined;\n}\n\n/**\n * Returns a current access token for the server, refreshing first when the stored one\n * is at the edge of expiry. Throws `McpOAuthRequiredError` when nothing is stored and\n * `McpOAuthRefreshError` when a refresh cannot be completed; both name the auth\n * command in their message.\n */\nexport async function ensureFreshServerToken(options: {\n\tserver: McpServerConfig;\n\tcredentials: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}): Promise<string> {\n\tconst { server, credentials } = options;\n\tconst now = options.now ?? Date.now;\n\tconst serverName = server.name;\n\tconst stored = await readServerToken(credentials, serverName);\n\tif (!stored) throw authRequired(serverName);\n\tif (stored.expiresAt - REFRESH_SKEW_MS > now()) return stored.accessToken;\n\tif (!stored.refreshToken) {\n\t\tthrow new McpOAuthRefreshError(\n\t\t\t`MCP server \"${serverName}\" authorization has expired and no refresh token was issued. Run \\`apex-code mcp auth ${serverName}\\` to authorize it again.`,\n\t\t\t\"refused\",\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new McpOAuthRefreshError(\n\t\t\t`MCP server \"${serverName}\" is not an HTTP server; run \\`apex-code mcp auth ${serverName}\\`.`,\n\t\t\t\"refused\",\n\t\t);\n\t}\n\tconst resource = stored.extras.resource ?? server.transport.url;\n\n\tlet tokenEndpoint = stored.extras.tokenEndpoint;\n\tif (!tokenEndpoint) {\n\t\tconst metadata = await discoverAuthorizationServer({ serverUrl: server.transport.url, fetch: options.fetch });\n\t\ttokenEndpoint = metadata.tokenEndpoint;\n\t}\n\n\tlet refreshed: TokenResponse;\n\ttry {\n\t\trefreshed = await refreshAccessToken({\n\t\t\ttokenEndpoint: new URL(tokenEndpoint),\n\t\t\trefreshToken: stored.refreshToken,\n\t\t\tclientId: stored.extras.clientId ?? `apex-code-mcp-${serverName}`,\n\t\t\tresource,\n\t\t\tfetch: options.fetch,\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof TokenEndpointError && error.code === \"invalid_grant\") {\n\t\t\tthrow new McpOAuthRefreshError(\n\t\t\t\t`MCP server \"${serverName}\" rejected its refresh token. Run \\`apex-code mcp auth ${serverName}\\` to authorize it again.`,\n\t\t\t\t\"refused\",\n\t\t\t);\n\t\t}\n\t\tif (error instanceof TokenEndpointUnreachableError) {\n\t\t\tthrow new McpOAuthRefreshError(`MCP server \"${serverName}\" refresh failed: ${error.message}`, \"unavailable\");\n\t\t}\n\t\tif (error instanceof TokenEndpointError) {\n\t\t\tthrow new McpOAuthRefreshError(`MCP server \"${serverName}\" refresh failed: ${error.message}`, \"refused\");\n\t\t}\n\t\tthrow new McpOAuthRefreshError(\n\t\t\t`MCP server \"${serverName}\" refresh failed: ${oauthErrorMessage(error)}`,\n\t\t\t\"unavailable\",\n\t\t);\n\t}\n\n\tconst expiresAt = now() + refreshed.expiresInSeconds * 1000 - REFRESH_SKEW_MS;\n\tconst credential: Credential = {\n\t\ttype: \"oauth\",\n\t\taccess: refreshed.accessToken,\n\t\t// No rotation: keep the refresh token the server still honors.\n\t\trefresh: refreshed.refreshToken ?? stored.refreshToken,\n\t\texpires: expiresAt,\n\t\ttokenEndpoint,\n\t\tresource,\n\t\tclientId: stored.extras.clientId,\n\t};\n\ttry {\n\t\tawait credentials.modify(mcpCredentialKey(serverName), async () => credential);\n\t} catch (error) {\n\t\t// The fresh access token works for this connection, but the store could not\n\t\t// record the rotation, which a read-only store does by throwing.\n\t\t// Fail this call closed rather than run ahead of what the store knows.\n\t\tthrow new McpOAuthRefreshError(\n\t\t\t`MCP server \"${serverName}\" was refreshed but the session cannot store the new token: ${oauthErrorMessage(error)}`,\n\t\t\t\"unavailable\",\n\t\t);\n\t}\n\treturn refreshed.accessToken;\n}\n"]}
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
* The MCP credential seam: how an MCP server's OAuth token is keyed, read, and
|
|
3
3
|
* refreshed through the session's existing `CredentialStore`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* store whose `modify` throws, which is the fail-closed path: refresh never
|
|
10
|
-
* silently degrades to "pretend it worked".
|
|
5
|
+
* That store is `AuthStorage` (direct, lock-serialized writes) unless an embedder
|
|
6
|
+
* supplies its own. A session with neither has a read-only store whose `modify`
|
|
7
|
+
* throws, which is the fail-closed path: refresh never silently degrades to
|
|
8
|
+
* "pretend it worked".
|
|
11
9
|
*/
|
|
12
10
|
import { discoverAuthorizationServer, oauthErrorMessage } from "./discover.js";
|
|
13
11
|
import { refreshAccessToken, TokenEndpointError, TokenEndpointUnreachableError, } from "./token-client.js";
|
|
@@ -120,7 +118,7 @@ export async function ensureFreshServerToken(options) {
|
|
|
120
118
|
}
|
|
121
119
|
catch (error) {
|
|
122
120
|
// The fresh access token works for this connection, but the store could not
|
|
123
|
-
// record the rotation
|
|
121
|
+
// record the rotation, which a read-only store does by throwing.
|
|
124
122
|
// Fail this call closed rather than run ahead of what the store knows.
|
|
125
123
|
throw new McpOAuthRefreshError(`MCP server "${serverName}" was refreshed but the session cannot store the new token: ${oauthErrorMessage(error)}`, "unavailable");
|
|
126
124
|
}
|