melchizedek-agents 1.1.0 → 1.3.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.
Files changed (146) hide show
  1. package/.env.example +19 -0
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +373 -0
  4. package/DOCUMENTATION.md +193 -21
  5. package/ONBOARDING.md +378 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +26 -3
  8. package/config/agents/examples/connectors.yaml +82 -0
  9. package/config/agents/examples/council.yaml +1 -1
  10. package/config/agents/examples/critic.yaml +8 -8
  11. package/config/agents/examples/image_production.yaml +4 -3
  12. package/config/agents/syndicate.schema.json +389 -33
  13. package/config/agents/syndicateSchema.yaml +18 -6
  14. package/config/agents/templates/intake_extractor.yaml +3 -2
  15. package/config/agents/templates/systems_operator.yaml +5 -2
  16. package/dist/lib/a2a/app.d.ts +12 -0
  17. package/dist/lib/a2a/app.js +25 -11
  18. package/dist/lib/a2a/executor.js +14 -6
  19. package/dist/lib/a2a/oauthSetup.d.ts +16 -0
  20. package/dist/lib/a2a/oauthSetup.js +77 -8
  21. package/dist/lib/agentDialect.d.ts +18 -8
  22. package/dist/lib/agentDialect.js +36 -19
  23. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  24. package/dist/lib/chatgpt/adapter.js +107 -0
  25. package/dist/lib/chatgpt/cli.d.ts +23 -0
  26. package/dist/lib/chatgpt/cli.js +77 -0
  27. package/dist/lib/chatgpt/oauth.d.ts +103 -0
  28. package/dist/lib/chatgpt/oauth.js +448 -0
  29. package/dist/lib/chatgpt/state.d.ts +98 -0
  30. package/dist/lib/chatgpt/state.js +145 -0
  31. package/dist/lib/chatgpt/store.d.ts +24 -0
  32. package/dist/lib/chatgpt/store.js +94 -0
  33. package/dist/lib/compile.d.ts +38 -7
  34. package/dist/lib/compile.js +97 -62
  35. package/dist/lib/compileNative.d.ts +11 -3
  36. package/dist/lib/compileNative.js +18 -1
  37. package/dist/lib/dispatch.js +6 -2
  38. package/dist/lib/doctor.d.ts +107 -0
  39. package/dist/lib/doctor.js +217 -7
  40. package/dist/lib/loadSyndicate.d.ts +15 -0
  41. package/dist/lib/mcp/server.d.ts +195 -0
  42. package/dist/lib/mcp/server.js +564 -0
  43. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  44. package/dist/lib/models/adapterResolver.js +6 -1
  45. package/dist/lib/models/capabilities.d.ts +9 -0
  46. package/dist/lib/models/capabilities.js +24 -0
  47. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  48. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  49. package/dist/lib/models/claudeAdapter.js +4 -0
  50. package/dist/lib/models/contract.d.ts +5 -2
  51. package/dist/lib/models/gateway.d.ts +3 -2
  52. package/dist/lib/models/gateway.js +8 -4
  53. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  54. package/dist/lib/models/gatewayAdapter.js +8 -0
  55. package/dist/lib/models/geminiAdapter.js +8 -2
  56. package/dist/lib/models/genaiMapping.d.ts +8 -4
  57. package/dist/lib/models/genaiMapping.js +23 -9
  58. package/dist/lib/models/gptAdapter.d.ts +4 -2
  59. package/dist/lib/models/gptAdapter.js +11 -4
  60. package/dist/lib/models/grokAdapter.d.ts +3 -1
  61. package/dist/lib/models/grokAdapter.js +11 -3
  62. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  63. package/dist/lib/models/kimiAdapter.js +3 -3
  64. package/dist/lib/models/reasoning.d.ts +30 -2
  65. package/dist/lib/models/reasoning.js +56 -5
  66. package/dist/lib/onboarding.d.ts +135 -0
  67. package/dist/lib/onboarding.js +578 -0
  68. package/dist/lib/runtime/approvals.d.ts +2 -0
  69. package/dist/lib/runtime/approvals.js +4 -3
  70. package/dist/lib/runtime/credentials.d.ts +6 -0
  71. package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
  72. package/dist/lib/runtime/native/agentLoop.js +2 -0
  73. package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
  74. package/dist/lib/runtime/native/checkpoint.js +13 -19
  75. package/dist/lib/runtime/native/delegate.d.ts +82 -1
  76. package/dist/lib/runtime/native/delegate.js +157 -2
  77. package/dist/lib/runtime/native/interrupts.d.ts +88 -5
  78. package/dist/lib/runtime/native/interrupts.js +220 -30
  79. package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
  80. package/dist/lib/runtime/syndicateTurn.js +324 -77
  81. package/dist/lib/runtime/turnControl.d.ts +41 -0
  82. package/dist/lib/session/transcript.d.ts +9 -0
  83. package/dist/lib/session/transcript.js +20 -1
  84. package/dist/lib/skills.d.ts +18 -0
  85. package/dist/lib/skills.js +73 -1
  86. package/dist/lib/storage/postgres/index.d.ts +6 -1
  87. package/dist/lib/storage/postgres/index.js +1 -1
  88. package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
  89. package/dist/lib/storage/postgres/taskQueue.js +10 -3
  90. package/dist/lib/syndicateSchema.d.ts +184 -9
  91. package/dist/lib/syndicateSchema.js +143 -29
  92. package/dist/lib/tools/auth.d.ts +13 -1
  93. package/dist/lib/tools/auth.js +10 -2
  94. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  95. package/dist/lib/tools/credentialHosts.js +165 -0
  96. package/dist/lib/tools/credentialUses.d.ts +98 -0
  97. package/dist/lib/tools/credentialUses.js +120 -0
  98. package/dist/lib/tools/generateImageTool.js +2 -1
  99. package/dist/lib/tools/inspectImageTool.js +1 -1
  100. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  101. package/dist/lib/tools/mcpToolFactory.js +72 -26
  102. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  103. package/dist/lib/tools/mcpTransports.js +11 -0
  104. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  105. package/dist/lib/tools/oauthConsent.js +57 -15
  106. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  107. package/dist/lib/tools/oauthDiscovery.js +616 -0
  108. package/dist/lib/tools/oauthHosts.d.ts +2 -0
  109. package/dist/lib/tools/oauthHosts.js +2 -2
  110. package/dist/lib/tools/oauthTools.d.ts +54 -13
  111. package/dist/lib/tools/oauthTools.js +106 -23
  112. package/dist/lib/tools/openapiTools.d.ts +7 -0
  113. package/dist/lib/tools/openapiTools.js +49 -3
  114. package/dist/lib/tools/taskTools.d.ts +32 -2
  115. package/dist/lib/tools/taskTools.js +93 -33
  116. package/dist/lib/tools/xApiSearchTool.js +2 -2
  117. package/dist/lib/workflow/turn.d.ts +22 -3
  118. package/dist/lib/workflow/turn.js +151 -31
  119. package/dist/lib/workflow.d.ts +10 -7
  120. package/dist/lib/workflow.js +10 -7
  121. package/dist/scripts/a2a_server.d.ts +2 -2
  122. package/dist/scripts/a2a_server.js +19 -2
  123. package/dist/scripts/assistant_worker.d.ts +2 -0
  124. package/dist/scripts/assistant_worker.js +24 -5
  125. package/dist/scripts/doctor.d.ts +4 -2
  126. package/dist/scripts/doctor.js +6 -4
  127. package/dist/scripts/mcp_server.d.ts +12 -0
  128. package/dist/scripts/mcp_server.js +345 -0
  129. package/dist/scripts/setup.d.ts +25 -0
  130. package/dist/scripts/setup.js +181 -0
  131. package/dist/scripts/skills_install.d.ts +3 -1
  132. package/dist/scripts/skills_install.js +17 -2
  133. package/dist/scripts/syndicate_chat.js +12 -3
  134. package/dist/scripts/yaml_codemod.d.ts +12 -2
  135. package/dist/scripts/yaml_codemod.js +115 -33
  136. package/package.json +8 -2
  137. package/skills/README.md +11 -2
  138. package/skills/briefs/_shared.md +1 -1
  139. package/skills/briefs/melchizedek-models.md +4 -2
  140. package/skills/briefs/skills-readme.md +3 -1
  141. package/skills/melchizedek-models/SKILL.md +6 -2
  142. package/skills/melchizedek-onboard/SKILL.md +71 -0
  143. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  144. package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
  145. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  146. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * lib/tools/credentialUses.ts — every static credential variable a syndicate
3
+ * YAML sends somewhere, and where (ADR 0122).
4
+ *
5
+ * A YAML sends a static credential in three places: an OpenAPI entry's
6
+ * `auth.bearer_env` or `auth.api_key.env` (to the spec's server, or
7
+ * `base_url`), and an `oauth2` block's `client_secret_env` (to its
8
+ * `token_url`; the OpenAPI entry's or the MCP server's `mcp_auth`). The
9
+ * operator's credential host allowlist (lib/tools/credentialHosts.ts) is
10
+ * checked against them when a served syndicate loads
11
+ * (`syndicateCredentialHostProblems`), and the doctor and the server binary
12
+ * name each variable no allowlist binds (`unboundCredentialEnvs`).
13
+ *
14
+ * Names and hosts only: no value is read here.
15
+ */
16
+ import type { CredentialHostAllowlist } from './credentialHosts.ts';
17
+ /** One credential variable a YAML sends, and to what. */
18
+ export interface CredentialUse {
19
+ /** The agent that holds the tools ("Parent › Child" inside a nested file). */
20
+ agent: string;
21
+ /** `openapi <spec>` or `mcp <url>`. */
22
+ tools: string;
23
+ /** The variable's name. */
24
+ env: string;
25
+ /** Where the YAML names it. */
26
+ role: 'bearer_env' | 'api_key' | 'client_secret_env';
27
+ /**
28
+ * The URLs its value goes to: the token endpoint for a client secret,
29
+ * `base_url` for an OpenAPI entry that sets one. Absent for an OpenAPI
30
+ * entry whose server is in its spec (read only when checked, by
31
+ * `syndicateCredentialHostProblems`).
32
+ */
33
+ destinations?: string[];
34
+ /** The OpenAPI entry, for its spec's servers. */
35
+ openapi?: OpenApiEntryLike;
36
+ }
37
+ /** The parts of an `oauth2` block read here. */
38
+ interface OAuth2Like {
39
+ token_url?: string;
40
+ client_secret_env?: string;
41
+ }
42
+ /** The parts of an `openapi:` entry read here. */
43
+ interface OpenApiEntryLike {
44
+ spec?: string;
45
+ base_url?: string;
46
+ auth?: {
47
+ bearer_env?: string;
48
+ api_key?: {
49
+ env?: string;
50
+ };
51
+ oauth2?: OAuth2Like;
52
+ };
53
+ }
54
+ interface AgentLike {
55
+ name?: string;
56
+ openapi?: OpenApiEntryLike[];
57
+ mcp_server_url?: string;
58
+ mcp_auth?: {
59
+ oauth2?: OAuth2Like;
60
+ };
61
+ mcp_servers?: Array<{
62
+ url?: string;
63
+ auth?: {
64
+ oauth2?: OAuth2Like;
65
+ };
66
+ }>;
67
+ yaml_reference?: string;
68
+ }
69
+ /** The parts of a syndicate config read here. */
70
+ export interface SyndicateLike {
71
+ orchestrator?: AgentLike;
72
+ subagents?: AgentLike[];
73
+ }
74
+ /** The credential variables one agent's tools send. */
75
+ export declare function agentCredentialUses(agent: AgentLike, prefix?: string): CredentialUse[];
76
+ /**
77
+ * Every credential variable these syndicates send, orchestrators and
78
+ * subagents, following a nested `yaml_reference:` through `load` when given
79
+ * (once per file; agent names then read "Parent › Child").
80
+ */
81
+ export declare function syndicateCredentialUses(configs: readonly SyndicateLike[], load?: (ref: string) => SyndicateLike): CredentialUse[];
82
+ /**
83
+ * Why these syndicates may not be served here, one line per refusal
84
+ * (ADR 0122): each credential variable they send, against the operator's
85
+ * credential host allowlist. With none configured, nothing is refused. The
86
+ * A2A server checks every syndicate it loads to serve; the compile checks
87
+ * again, and each call once more.
88
+ */
89
+ export declare function syndicateCredentialHostProblems(configs: readonly SyndicateLike[], options?: {
90
+ load?: (ref: string) => SyndicateLike;
91
+ allowlist?: CredentialHostAllowlist | null;
92
+ }): string[];
93
+ /**
94
+ * The credential variables these uses send that no allowlist binds, sorted
95
+ * and once each: every one of them when none is configured. Names only.
96
+ */
97
+ export declare function unboundCredentialEnvs(uses: readonly CredentialUse[], allowlist: CredentialHostAllowlist | null | undefined): string[];
98
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * lib/tools/credentialUses.ts — every static credential variable a syndicate
3
+ * YAML sends somewhere, and where (ADR 0122).
4
+ *
5
+ * A YAML sends a static credential in three places: an OpenAPI entry's
6
+ * `auth.bearer_env` or `auth.api_key.env` (to the spec's server, or
7
+ * `base_url`), and an `oauth2` block's `client_secret_env` (to its
8
+ * `token_url`; the OpenAPI entry's or the MCP server's `mcp_auth`). The
9
+ * operator's credential host allowlist (lib/tools/credentialHosts.ts) is
10
+ * checked against them when a served syndicate loads
11
+ * (`syndicateCredentialHostProblems`), and the doctor and the server binary
12
+ * name each variable no allowlist binds (`unboundCredentialEnvs`).
13
+ *
14
+ * Names and hosts only: no value is read here.
15
+ */
16
+ import { resolve } from 'node:path';
17
+ import { credentialHostProblem, credentialHosts } from './credentialHosts.js';
18
+ import { openApiServers } from './openapiTools.js';
19
+ /** The token endpoint a client secret goes to; none named yet for a grant whose endpoints are discovered (ADR 0124). */
20
+ const tokenUrlOf = (o) => (o.token_url ? [o.token_url] : []);
21
+ /** The credential variables one agent's tools send. */
22
+ export function agentCredentialUses(agent, prefix = '') {
23
+ const name = `${prefix}${agent.name ?? ''}`;
24
+ const out = [];
25
+ for (const entry of agent.openapi ?? []) {
26
+ const tools = `openapi ${entry?.spec ?? ''}`;
27
+ const auth = entry?.auth;
28
+ if (!auth)
29
+ continue;
30
+ const servers = entry.base_url ? { destinations: [entry.base_url] } : {};
31
+ if (auth.bearer_env)
32
+ out.push({ agent: name, tools, env: auth.bearer_env, role: 'bearer_env', ...servers, openapi: entry });
33
+ if (auth.api_key?.env)
34
+ out.push({ agent: name, tools, env: auth.api_key.env, role: 'api_key', ...servers, openapi: entry });
35
+ if (auth.oauth2?.client_secret_env)
36
+ out.push({ agent: name, tools, env: auth.oauth2.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(auth.oauth2) });
37
+ }
38
+ const mcp = agent.mcp_auth?.oauth2;
39
+ if (mcp?.client_secret_env)
40
+ out.push({ agent: name, tools: `mcp ${agent.mcp_server_url ?? ''}`, env: mcp.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(mcp) });
41
+ // Each mcp_servers entry's grant (ADR 0124), as the single server's.
42
+ for (const server of agent.mcp_servers ?? []) {
43
+ const o = server?.auth?.oauth2;
44
+ if (o?.client_secret_env)
45
+ out.push({ agent: name, tools: `mcp ${server.url ?? ''}`, env: o.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(o) });
46
+ }
47
+ return out;
48
+ }
49
+ /**
50
+ * Every credential variable these syndicates send, orchestrators and
51
+ * subagents, following a nested `yaml_reference:` through `load` when given
52
+ * (once per file; agent names then read "Parent › Child").
53
+ */
54
+ export function syndicateCredentialUses(configs, load) {
55
+ const out = [];
56
+ const seen = new Set();
57
+ const visit = (config, prefix) => {
58
+ const agents = [config.orchestrator, ...(config.subagents ?? [])].filter((a) => !!a);
59
+ for (const agent of agents) {
60
+ if (agent.yaml_reference) {
61
+ if (!load || seen.has(agent.yaml_reference))
62
+ continue;
63
+ seen.add(agent.yaml_reference);
64
+ visit(load(agent.yaml_reference), `${prefix}${agent.name ?? agent.yaml_reference} › `);
65
+ continue;
66
+ }
67
+ out.push(...agentCredentialUses(agent, prefix));
68
+ }
69
+ };
70
+ for (const config of configs)
71
+ visit(config, '');
72
+ return out;
73
+ }
74
+ /** The URLs a use's value goes to; an OpenAPI spec that cannot be read gives none (its compile fails on its own). */
75
+ function destinationsOf(use) {
76
+ if (use.destinations)
77
+ return use.destinations;
78
+ if (!use.openapi?.spec)
79
+ return [];
80
+ try {
81
+ return openApiServers(use.openapi, resolve(process.cwd()));
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ }
87
+ /**
88
+ * Why these syndicates may not be served here, one line per refusal
89
+ * (ADR 0122): each credential variable they send, against the operator's
90
+ * credential host allowlist. With none configured, nothing is refused. The
91
+ * A2A server checks every syndicate it loads to serve; the compile checks
92
+ * again, and each call once more.
93
+ */
94
+ export function syndicateCredentialHostProblems(configs, options = {}) {
95
+ const allowlist = options.allowlist === undefined ? credentialHosts() ?? null : options.allowlist;
96
+ if (!allowlist)
97
+ return [];
98
+ const out = new Set();
99
+ for (const use of syndicateCredentialUses(configs, options.load)) {
100
+ const role = use.role === 'client_secret_env' ? 'token_url' : 'server';
101
+ const where = `${use.agent} · ${use.tools} · ${use.role}`;
102
+ if (!Object.hasOwn(allowlist, use.env)) {
103
+ out.add(`${where}: ${credentialHostProblem(use.env, '', role, allowlist)}`);
104
+ continue;
105
+ }
106
+ for (const url of destinationsOf(use)) {
107
+ const problem = credentialHostProblem(use.env, url, role, allowlist);
108
+ if (problem)
109
+ out.add(`${where}: ${problem}`);
110
+ }
111
+ }
112
+ return [...out];
113
+ }
114
+ /**
115
+ * The credential variables these uses send that no allowlist binds, sorted
116
+ * and once each: every one of them when none is configured. Names only.
117
+ */
118
+ export function unboundCredentialEnvs(uses, allowlist) {
119
+ return [...new Set(uses.map((u) => u.env))].filter((name) => !allowlist || !Object.hasOwn(allowlist, name)).sort();
120
+ }
@@ -38,7 +38,8 @@ export const generateImageContract = defineTool({
38
38
  console.error(`[ImageTool ERROR] Initialization failed: ${err.message}`);
39
39
  throw err;
40
40
  }
41
- console.log(`[ImageTool] Parameters - Prompt: "${prompt}", Style: "${style ?? 'none'}", Aspect Ratio: "${aspect_ratio ?? 'none'}", Color Palette: "${color_palette ?? 'none'}"`);
41
+ // Sizes and which options are set, never their values: the prompt carries the person's words.
42
+ console.log(`[ImageTool] Parameters - prompt ${prompt.length} chars; set: ${[style && 'style', aspect_ratio && 'aspect_ratio', color_palette && 'color_palette'].filter(Boolean).join(', ') || 'none'}`);
42
43
  const ai = new GoogleGenAI({ apiKey });
43
44
  // Build a rich prompt that bakes in the structured parameters.
44
45
  const parts = [prompt];
@@ -46,7 +46,7 @@ export const inspectImageContract = defineTool({
46
46
  image_path: z.string().describe('Path to the saved image, e.g. "outputs/image_1234.png".'),
47
47
  }),
48
48
  execute: async ({ image_path }) => {
49
- console.log(`[InspectTool] Blind inventory requested for: ${image_path}`);
49
+ console.log(`[InspectTool] Blind inventory requested (path ${image_path.length} chars).`);
50
50
  // Confine reads to the outputs/ directory — this tool inventories
51
51
  // generated artifacts, not the filesystem. OUTPUTS_DIR env overrides
52
52
  // the cwd default (must match generateImageTool's).
@@ -1,6 +1,8 @@
1
1
  import type { ToolDeclaration } from '../models/contract.ts';
2
2
  import type { RedirectPolicy } from '../net/redirects.ts';
3
3
  import type { OAuth2AuthConfig } from './oauthTools.ts';
4
+ import { MCP_TRANSPORTS } from './mcpTransports.ts';
5
+ import type { McpTransportKind } from './mcpTransports.ts';
4
6
  import type { Tool } from './tool.ts';
5
7
  export declare function assertSafeMcpUrl(raw: string, env?: NodeJS.ProcessEnv): Promise<URL>;
6
8
  export declare function mcpAuthHeaders(url: URL, env?: NodeJS.ProcessEnv): Record<string, string>;
@@ -23,6 +25,15 @@ export declare const mcpFetch: (input: string | URL | Request, init?: RequestIni
23
25
  export declare const MAX_MCP_DESCRIPTION_CHARS = 1000;
24
26
  /** Every tool's shared result limit (lib/tools/tool.ts). */
25
27
  export declare const MAX_MCP_RESULT_CHARS = 20000;
28
+ export { MCP_TRANSPORTS };
29
+ export type { McpTransportKind };
30
+ /**
31
+ * True when a failed Streamable HTTP connect is the spec's signal that the
32
+ * server speaks only the older HTTP+SSE transport: the initialize POST
33
+ * answered with a 4xx (404, 405, 400 …). A 401 or 403 is the server refusing
34
+ * the credential, which another transport would not change.
35
+ */
36
+ export declare function isSseFallbackSignal(error: unknown): boolean;
26
37
  /**
27
38
  * Close every open MCP connection. A connection otherwise lives as long as
28
39
  * the process (its tools are called for the life of the compiled agent), and
@@ -44,10 +55,27 @@ export declare function mcpToolParameters(inputSchema: {
44
55
  } | undefined): ToolDeclaration['parameters'];
45
56
  /** How an agent reaches its MCP server: which tools it may use, and the OAuth grant the server takes. */
46
57
  export interface McpToolOptions {
47
- /** `mcp_tools`: the names this agent may use. Required with an authorization_code grant. */
58
+ /** `mcp_tools` (or an `mcp_servers` entry's `tools`): the names this agent may use. Required with an authorization_code grant. */
48
59
  tools?: string[];
49
60
  /** `mcp_auth.oauth2` (ADR 0112). Without it, MCP_BEARER_TOKENS applies. */
50
61
  oauth2?: OAuth2AuthConfig;
62
+ /** `mcp_transport` (or an entry's `transport`): default `auto` (ADR 0124). */
63
+ transport?: McpTransportKind;
64
+ }
65
+ /**
66
+ * One entry of an agent's `mcp_servers:` list (ADR 0124): a server, the
67
+ * tools of it the agent may use, the grant it takes and its transport.
68
+ */
69
+ export interface McpServerConfig {
70
+ /** A short name for the server, unique on the agent: logs and errors name it. */
71
+ name: string;
72
+ url: string;
73
+ /** The server's tools this agent may use, by name. Required: names across servers are refused on collision at load. */
74
+ tools: string[];
75
+ auth?: {
76
+ oauth2: OAuth2AuthConfig;
77
+ };
78
+ transport?: McpTransportKind;
51
79
  }
52
80
  /** Per-user connections at most per authorization-code server; the least recently used is closed first. */
53
81
  export declare const MAX_MCP_USER_CONNECTIONS = 64;
@@ -2,8 +2,11 @@
2
2
  * lib/tools/mcpToolFactory.ts — a remote MCP server's tools, as the
3
3
  * engine's own Tools (lib/tools/tool.ts, ADR 0062).
4
4
  *
5
- * An agent's `mcp_server_url:` connects here over SSE; every tool the
6
- * server lists becomes an own Tool whose declaration is the server's
5
+ * An agent's `mcp_server_url:` (or each entry of `mcp_servers:`, ADR 0124)
6
+ * connects here over Streamable HTTP, falling back to the legacy SSE
7
+ * transport when the server answers the initialize POST with the spec's
8
+ * fallback signal (a 4xx other than 401/403), or over the transport the YAML
9
+ * names. Every tool the server lists becomes an own Tool whose declaration is the server's
7
10
  * description (bounded) and input schema, and whose execute calls the
8
11
  * server and returns its text (bounded). loadMcpTools returns them;
9
12
  * createMcpTools is the name the compiler calls.
@@ -20,11 +23,13 @@
20
23
  */
21
24
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
22
25
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
26
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
23
27
  import { toContractJsonSchema } from '../models/schemaNormalize.js';
24
28
  import { checkHost } from '../net/addressGuard.js';
25
29
  import { fetchWithRedirectPolicy } from '../net/redirects.js';
26
30
  import { ToolCredentialError } from './auth.js';
27
31
  import { oauthCallProblem, oauthTokenSource } from './oauthTools.js';
32
+ import { MCP_TRANSPORTS } from './mcpTransports.js';
28
33
  import { MAX_RESULT_CHARS } from './tool.js';
29
34
  import { toGeminiSchema } from './toolContract.js';
30
35
  // Security (SSRF): mcp_server_url can arrive from a registry-stored syndicate
@@ -116,6 +121,54 @@ export const MAX_MCP_RESULT_CHARS = MAX_RESULT_CHARS;
116
121
  const bounded = (text, max, what) => text.length <= max ? text : `${text.slice(0, max)}… [${what} cut at ${max} characters]`;
117
122
  /** Every MCP connection opened and still in use, so a shutdown can close them. */
118
123
  const openTransports = new Set();
124
+ export { MCP_TRANSPORTS };
125
+ /**
126
+ * True when a failed Streamable HTTP connect is the spec's signal that the
127
+ * server speaks only the older HTTP+SSE transport: the initialize POST
128
+ * answered with a 4xx (404, 405, 400 …). A 401 or 403 is the server refusing
129
+ * the credential, which another transport would not change.
130
+ */
131
+ export function isSseFallbackSignal(error) {
132
+ if (!(error instanceof StreamableHTTPError))
133
+ return false;
134
+ const code = error.code;
135
+ return typeof code === 'number' && code >= 400 && code < 500 && code !== 401 && code !== 403;
136
+ }
137
+ /**
138
+ * A client connected to `url` over `kind`, every request through `fetchFn`
139
+ * (the redirect rule, and the credential when there is one). A transport
140
+ * that fails to connect is closed before the error leaves, so nothing keeps
141
+ * reconnecting; with `auto`, a failed Streamable HTTP attempt carrying the
142
+ * fallback signal is closed and the SSE transport tried once.
143
+ */
144
+ async function connectMcp(url, kind, fetchFn, headers = {}) {
145
+ const attempt = async (which) => {
146
+ const requestInit = Object.keys(headers).length ? { requestInit: { headers } } : {};
147
+ const transport = which === 'sse' ? new SSEClientTransport(url, { ...requestInit, fetch: fetchFn }) : new StreamableHTTPClientTransport(url, { ...requestInit, fetch: fetchFn });
148
+ const client = new Client({ name: 'melchizedek-a2a-client', version: '1.0.0' }, { capabilities: {} });
149
+ try {
150
+ await client.connect(transport);
151
+ }
152
+ catch (error) {
153
+ await transport.close().catch(() => { });
154
+ throw error;
155
+ }
156
+ openTransports.add(transport);
157
+ return { client, transport, kind: which };
158
+ };
159
+ if (kind === 'sse')
160
+ return attempt('sse');
161
+ if (kind === 'streamable_http')
162
+ return attempt('streamable_http');
163
+ try {
164
+ return await attempt('streamable_http');
165
+ }
166
+ catch (error) {
167
+ if (!isSseFallbackSignal(error))
168
+ throw error;
169
+ return attempt('sse');
170
+ }
171
+ }
119
172
  /**
120
173
  * Close every open MCP connection. A connection otherwise lives as long as
121
174
  * the process (its tools are called for the life of the compiled agent), and
@@ -174,7 +227,7 @@ function bearerFetch(oauth2, token) {
174
227
  // message endpoint the server names), before a token is attached (ADR 0114).
175
228
  const destination = input instanceof Request ? input.url : String(input);
176
229
  if (oauthCallProblem(oauth2, destination))
177
- throw new ToolCredentialError('host_refused', oauth2.provider);
230
+ throw new ToolCredentialError('host_refused', oauth2.provider, 'oauth');
178
231
  const headers = new Headers(init?.headers);
179
232
  headers.set('Authorization', `Bearer ${await token(destination)}`);
180
233
  return mcpFetch(input, { ...init, headers });
@@ -204,23 +257,17 @@ const userPools = new Set();
204
257
  */
205
258
  export async function loadMcpTools(mcpServerUrl, options = {}) {
206
259
  const oauth2 = options.oauth2;
260
+ const kind = options.transport ?? 'auto';
207
261
  if (oauth2?.grant === 'authorization_code')
208
- return userGrantMcpTools(mcpServerUrl, oauth2, options.tools);
262
+ return userGrantMcpTools(mcpServerUrl, oauth2, options.tools, kind);
209
263
  const tokenSource = oauth2 ? oauthTokenSource(oauth2, `mcp ${mcpServerUrl}`, mcpServerUrl, { allowPrivate: process.env.ALLOW_PRIVATE_MCP === 'true' }) : undefined;
210
- let transport;
264
+ let conn;
211
265
  try {
212
266
  const url = await assertSafeMcpUrl(mcpServerUrl);
213
- transport = new SSEClientTransport(url, tokenSource
214
- ? { fetch: bearerFetch(oauth2, (destination) => tokenSource(undefined, destination)) }
215
- : { requestInit: { headers: mcpAuthHeaders(url) }, fetch: mcpFetch });
216
- const client = new Client({
217
- name: 'melchizedek-a2a-client',
218
- version: '1.0.0'
219
- }, {
220
- capabilities: {}
221
- });
222
- await client.connect(transport);
223
- openTransports.add(transport);
267
+ conn = tokenSource
268
+ ? await connectMcp(url, kind, bearerFetch(oauth2, (destination) => tokenSource(undefined, destination)))
269
+ : await connectMcp(url, kind, mcpFetch, mcpAuthHeaders(url));
270
+ const client = conn.client;
224
271
  // Fetch available tools from the MCP server
225
272
  const toolsResponse = await client.listTools();
226
273
  return toolsResponse.tools.map((tool) => {
@@ -244,16 +291,18 @@ export async function loadMcpTools(mcpServerUrl, options = {}) {
244
291
  catch (error) {
245
292
  // A token that cannot be had is reported by kind; the rest as before.
246
293
  console.warn(`[MCP] Failed to connect or load tools from ${mcpServerUrl}`, error instanceof ToolCredentialError ? error.message : error);
247
- // A failed connect leaves the SSE stream's reconnect timer running; close
248
- // it, or every unreachable server keeps retrying for the process's life.
249
- if (transport)
250
- openTransports.delete(transport);
251
- await transport?.close().catch(() => { });
294
+ // A failed connect closed its own transport (connectMcp); a failed
295
+ // listing closes this one, or its stream keeps reconnecting for the
296
+ // process's life.
297
+ if (conn) {
298
+ openTransports.delete(conn.transport);
299
+ await conn.transport.close().catch(() => { });
300
+ }
252
301
  return [];
253
302
  }
254
303
  }
255
304
  /** The tools of a server whose calls carry each user's own OAuth token (authorization_code). */
256
- function userGrantMcpTools(mcpServerUrl, oauth2, names) {
305
+ function userGrantMcpTools(mcpServerUrl, oauth2, names, kind) {
257
306
  const where = `mcp ${mcpServerUrl}`;
258
307
  if (!names?.length)
259
308
  throw new Error(`${where}: an authorization_code mcp_auth needs mcp_tools, the names this agent may use (no user's grant exists to list them at startup)`);
@@ -286,11 +335,8 @@ function userGrantMcpTools(mcpServerUrl, oauth2, names) {
286
335
  conn.holder = holder;
287
336
  conn.ready = (async () => {
288
337
  const url = await assertSafeMcpUrl(mcpServerUrl);
289
- const transport = new SSEClientTransport(url, { fetch: bearerFetch(oauth2, async () => holder.token) });
338
+ const { client, transport } = await connectMcp(url, kind, bearerFetch(oauth2, async () => holder.token));
290
339
  conn.transport = transport;
291
- const client = new Client({ name: 'melchizedek-a2a-client', version: '1.0.0' }, { capabilities: {} });
292
- await client.connect(transport);
293
- openTransports.add(transport);
294
340
  if (!listed) {
295
341
  const offered = await client.listTools();
296
342
  for (const tool of offered.tools)
@@ -0,0 +1,12 @@
1
+ /**
2
+ * lib/tools/mcpTransports.ts — the transports an agent may name for an MCP
3
+ * server (ADR 0124), in a module light enough for the YAML schema to import
4
+ * without loading the MCP SDK.
5
+ *
6
+ * `auto` (the default) posts the initialize request over Streamable HTTP, the
7
+ * spec's current transport, and falls back to the legacy HTTP+SSE transport
8
+ * on the spec's signal (lib/tools/mcpToolFactory.ts isSseFallbackSignal);
9
+ * `streamable_http` and `sse` use that one transport only.
10
+ */
11
+ export declare const MCP_TRANSPORTS: readonly ['auto', 'streamable_http', 'sse'];
12
+ export type McpTransportKind = (typeof MCP_TRANSPORTS)[number];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * lib/tools/mcpTransports.ts — the transports an agent may name for an MCP
3
+ * server (ADR 0124), in a module light enough for the YAML schema to import
4
+ * without loading the MCP SDK.
5
+ *
6
+ * `auto` (the default) posts the initialize request over Streamable HTTP, the
7
+ * spec's current transport, and falls back to the legacy HTTP+SSE transport
8
+ * on the spec's signal (lib/tools/mcpToolFactory.ts isSseFallbackSignal);
9
+ * `streamable_http` and `sse` use that one transport only.
10
+ */
11
+ export const MCP_TRANSPORTS = ['auto', 'streamable_http', 'sse'];
@@ -50,10 +50,23 @@ export interface OAuthClientConfig {
50
50
  scopes?: string[];
51
51
  /** Extra authorization parameters the provider needs (`access_type=offline`, say). Never `state`, `redirect_uri` or the PKCE pair. */
52
52
  authorizationParams?: Record<string, string>;
53
+ /**
54
+ * The protected resource the token is for (RFC 8707): sent as `resource`
55
+ * on the authorization request, the code exchange and each refresh. An MCP
56
+ * server's canonical URI, for a client registered by discovery (ADR 0124).
57
+ */
58
+ resource?: string;
53
59
  }
60
+ /**
61
+ * A client found when first needed rather than configured at boot: an MCP
62
+ * server's authorization server, discovered and registered at
63
+ * (lib/tools/oauthDiscovery.ts dynamicOAuthClient, ADR 0124). Throws when
64
+ * it cannot be had; the next call tries again.
65
+ */
66
+ export type OAuthClientSource = () => Promise<OAuthClientConfig>;
54
67
  export interface OAuthConsentOptions {
55
- /** Each provider's client, by provider name (lowercase, as the credential store names it). */
56
- providers: Record<string, OAuthClientConfig>;
68
+ /** Each provider's client, by provider name (lowercase, as the credential store names it): configured, or found when first needed. */
69
+ providers: Record<string, OAuthClientConfig | OAuthClientSource>;
57
70
  /**
58
71
  * The server's callback URL, exactly as registered at every provider
59
72
  * (`https://agents.example.com/oauth/callback`). The A2A server mounts the
@@ -122,7 +135,9 @@ export type ConsentErrorCode =
122
135
  /** The grant could not be stored. */
123
136
  | 'store_failed'
124
137
  /** The provider has no client configured. */
125
- | 'unknown_provider';
138
+ | 'unknown_provider'
139
+ /** A discovered client could not be found or registered (ADR 0124). */
140
+ | 'registration_failed';
126
141
  /** A consent step that cannot proceed. The message names the reason, never a value. */
127
142
  export declare class ConsentError extends Error {
128
143
  readonly code: ConsentErrorCode;
@@ -38,6 +38,7 @@
38
38
  import { createHash, randomBytes } from 'node:crypto';
39
39
  import { scopeHashOf } from '../observability/audit.js';
40
40
  import { PROVIDER_NAME } from './auth.js';
41
+ import { clientSecretEnvOf, credentialCallProblem } from './credentialHosts.js';
41
42
  /**
42
43
  * Flows in this process's memory, at most `max` of them: the oldest is
43
44
  * dropped first, and an expired one whenever another is put.
@@ -84,7 +85,9 @@ export class ConsentError extends Error {
84
85
  ? `The authorization at ${p} could not be completed.`
85
86
  : code === 'store_failed'
86
87
  ? `The authorization at ${p} could not be saved.`
87
- : `No OAuth client is configured for ${p}.`);
88
+ : code === 'registration_failed'
89
+ ? `The authorization at ${p} could not be set up.`
90
+ : `No OAuth client is configured for ${p}.`);
88
91
  this.name = 'ConsentError';
89
92
  this.code = code;
90
93
  if (provider !== undefined)
@@ -119,7 +122,21 @@ function checkEndpoint(value, what) {
119
122
  throw new Error(`${what} must not carry credentials.`);
120
123
  return url;
121
124
  }
122
- const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope']);
125
+ const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope', 'resource']);
126
+ /** A client's configuration, checked: the endpoints, the client id, the parameters. Throws, naming the provider. */
127
+ function checkClient(name, client) {
128
+ checkEndpoint(client.authorizationUrl, `${name}: authorizationUrl`);
129
+ checkEndpoint(client.tokenUrl, `${name}: tokenUrl`);
130
+ if (typeof client.clientId !== 'string' || !client.clientId)
131
+ throw new Error(`${name}: clientId is required.`);
132
+ for (const key of Object.keys(client.authorizationParams ?? {})) {
133
+ if (RESERVED_PARAMS.has(key))
134
+ throw new Error(`${name}: authorizationParams may not set "${key}".`);
135
+ }
136
+ if (client.resource !== undefined)
137
+ checkEndpoint(client.resource, `${name}: resource`);
138
+ return { ...client, scopes: [...(client.scopes ?? [])] };
139
+ }
123
140
  /** The consent manager over a credential store. Validates the configuration at once: a bad URL throws here, at boot. */
124
141
  export function oauthConsent(options) {
125
142
  const redirect = checkEndpoint(options.redirectUri, 'The OAuth redirect URI');
@@ -129,16 +146,23 @@ export function oauthConsent(options) {
129
146
  for (const [name, client] of Object.entries(options.providers)) {
130
147
  if (!PROVIDER_NAME.test(name))
131
148
  throw new Error(`OAuth provider "${name.slice(0, 64)}" must match ${PROVIDER_NAME}.`);
132
- checkEndpoint(client.authorizationUrl, `${name}: authorizationUrl`);
133
- checkEndpoint(client.tokenUrl, `${name}: tokenUrl`);
134
- if (typeof client.clientId !== 'string' || !client.clientId)
135
- throw new Error(`${name}: clientId is required.`);
136
- for (const key of Object.keys(client.authorizationParams ?? {})) {
137
- if (RESERVED_PARAMS.has(key))
138
- throw new Error(`${name}: authorizationParams may not set "${key}".`);
139
- }
140
- providers.set(name, { ...client, scopes: [...(client.scopes ?? [])] });
149
+ // A source is checked each time it answers; a configured client here, at boot.
150
+ providers.set(name, typeof client === 'function' ? client : checkClient(name, client));
141
151
  }
152
+ /** The provider's client now, or undefined; a source that fails is a ConsentError('registration_failed'). */
153
+ const clientOf = async (name) => {
154
+ const entry = providers.get(name);
155
+ if (typeof entry !== 'function')
156
+ return entry;
157
+ try {
158
+ return checkClient(name, await entry());
159
+ }
160
+ catch (err) {
161
+ // A discovery refusal names roles and hosts, never a value (lib/tools/oauthDiscovery.ts); anything else is named by kind only.
162
+ console.warn(`[OAuth] ${name}: the client could not be set up: ${err instanceof Error && err.name === 'OAuthDiscoveryError' ? err.message : err instanceof Error ? err.name : 'unknown error'}`);
163
+ throw new ConsentError('registration_failed', name);
164
+ }
165
+ };
142
166
  const states = options.states ?? memoryConsentStates({ ...(options.now ? { now: options.now } : {}) });
143
167
  const ttlMs = options.ttlMs ?? 10 * 60_000;
144
168
  const now = options.now ?? Date.now;
@@ -192,8 +216,16 @@ export function oauthConsent(options) {
192
216
  client_id: client.clientId,
193
217
  code_verifier: flow.codeVerifier,
194
218
  });
195
- if (client.clientSecret)
219
+ if (client.resource)
220
+ body.set('resource', client.resource);
221
+ if (client.clientSecret) {
222
+ // The secret goes only to a host the operator binds its variable to (ADR 0122), checked now:
223
+ // the allowlist can change after boot. Refused, nothing is sent.
224
+ const secretEnv = clientSecretEnvOf(client);
225
+ if (secretEnv && credentialCallProblem(secretEnv, client.tokenUrl))
226
+ throw new ConsentError('exchange_failed', flow.provider);
196
227
  body.set('client_secret', client.clientSecret);
228
+ }
197
229
  let json;
198
230
  try {
199
231
  const res = await doFetch(client.tokenUrl, {
@@ -231,9 +263,9 @@ export function oauthConsent(options) {
231
263
  redirectUri: options.redirectUri,
232
264
  has: (provider) => typeof provider === 'string' && providers.has(provider),
233
265
  async begin(binding) {
234
- const client = providers.get(binding.provider);
235
- if (!client)
266
+ if (!providers.has(binding.provider))
236
267
  throw new ConsentError('unknown_provider', PROVIDER_NAME.test(binding.provider) ? binding.provider : undefined);
268
+ const client = (await clientOf(binding.provider));
237
269
  for (const field of ['appName', 'userId', 'sessionId', 'functionCallId']) {
238
270
  if (typeof binding[field] !== 'string' || !binding[field])
239
271
  throw new Error(`A consent flow needs the paused call's ${field}.`);
@@ -262,6 +294,8 @@ export function oauthConsent(options) {
262
294
  url.searchParams.set('state', state);
263
295
  url.searchParams.set('code_challenge', s256Challenge(codeVerifier));
264
296
  url.searchParams.set('code_challenge_method', 'S256');
297
+ if (client.resource)
298
+ url.searchParams.set('resource', client.resource);
265
299
  const authUri = url.toString();
266
300
  const authConfig = {
267
301
  credentialKey: binding.provider,
@@ -307,7 +341,15 @@ export function oauthConsent(options) {
307
341
  const code = input.code;
308
342
  if (typeof code !== 'string' || !CODE_FORMAT.test(code))
309
343
  fail('invalid_request');
310
- const client = providers.get(flow.provider);
344
+ if (!providers.has(flow.provider))
345
+ fail('unknown_provider');
346
+ let client;
347
+ try {
348
+ client = await clientOf(flow.provider);
349
+ }
350
+ catch {
351
+ return fail('registration_failed');
352
+ }
311
353
  if (!client)
312
354
  fail('unknown_provider');
313
355
  let tokens;