melchizedek-agents 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.env.example +9 -0
  2. package/CHANGELOG.md +174 -0
  3. package/DOCUMENTATION.md +107 -10
  4. package/ONBOARDING.md +50 -11
  5. package/QUICKSTART.md +1 -1
  6. package/README.md +22 -2
  7. package/config/agents/examples/connectors.yaml +82 -0
  8. package/config/agents/syndicate.schema.json +324 -26
  9. package/dist/lib/a2a/app.js +3 -0
  10. package/dist/lib/a2a/oauthSetup.js +20 -5
  11. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  12. package/dist/lib/chatgpt/adapter.js +107 -0
  13. package/dist/lib/chatgpt/cli.d.ts +23 -0
  14. package/dist/lib/chatgpt/cli.js +77 -0
  15. package/dist/lib/chatgpt/oauth.d.ts +104 -0
  16. package/dist/lib/chatgpt/oauth.js +472 -0
  17. package/dist/lib/chatgpt/state.d.ts +98 -0
  18. package/dist/lib/chatgpt/state.js +145 -0
  19. package/dist/lib/chatgpt/store.d.ts +24 -0
  20. package/dist/lib/chatgpt/store.js +94 -0
  21. package/dist/lib/compile.js +40 -25
  22. package/dist/lib/dispatch.js +6 -2
  23. package/dist/lib/doctor.d.ts +34 -3
  24. package/dist/lib/doctor.js +63 -5
  25. package/dist/lib/loadSyndicate.d.ts +6 -0
  26. package/dist/lib/mcp/server.d.ts +195 -0
  27. package/dist/lib/mcp/server.js +564 -0
  28. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  29. package/dist/lib/models/adapterResolver.js +6 -1
  30. package/dist/lib/models/gateway.d.ts +1 -1
  31. package/dist/lib/models/gateway.js +4 -2
  32. package/dist/lib/models/gptAdapter.js +11 -1
  33. package/dist/lib/onboarding.d.ts +4 -3
  34. package/dist/lib/onboarding.js +48 -9
  35. package/dist/lib/runtime/syndicateTurn.js +30 -16
  36. package/dist/lib/syndicateSchema.d.ts +146 -8
  37. package/dist/lib/syndicateSchema.js +118 -11
  38. package/dist/lib/tools/credentialUses.d.ts +7 -1
  39. package/dist/lib/tools/credentialUses.js +10 -2
  40. package/dist/lib/tools/generateImageTool.js +2 -1
  41. package/dist/lib/tools/inspectImageTool.js +1 -1
  42. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  43. package/dist/lib/tools/mcpToolFactory.js +71 -25
  44. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  45. package/dist/lib/tools/mcpTransports.js +11 -0
  46. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  47. package/dist/lib/tools/oauthConsent.js +49 -14
  48. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  49. package/dist/lib/tools/oauthDiscovery.js +616 -0
  50. package/dist/lib/tools/oauthTools.d.ts +44 -12
  51. package/dist/lib/tools/oauthTools.js +79 -22
  52. package/dist/lib/tools/xApiSearchTool.js +2 -2
  53. package/dist/scripts/a2a_server.js +8 -0
  54. package/dist/scripts/assistant_worker.js +11 -1
  55. package/dist/scripts/doctor.d.ts +1 -1
  56. package/dist/scripts/doctor.js +1 -1
  57. package/dist/scripts/mcp_server.d.ts +12 -0
  58. package/dist/scripts/mcp_server.js +345 -0
  59. package/dist/scripts/setup.d.ts +2 -0
  60. package/dist/scripts/setup.js +27 -0
  61. package/dist/scripts/syndicate_chat.js +12 -3
  62. package/package.json +15 -12
  63. package/skills/README.md +2 -2
  64. package/skills/briefs/skills-readme.md +1 -1
  65. package/skills/melchizedek-onboard/SKILL.md +28 -6
  66. package/skills/melchizedek-onboard-keys/SKILL.md +17 -5
@@ -26,6 +26,7 @@ import { DEFAULT_ROUTE_KEY, NODE_KINDS, ROUTE_STEP_SUFFIX, START_NAME, elementNa
26
26
  import { isSecretShapedEnvName } from './tools/skills/env.js';
27
27
  import { OAUTH_SCOPE, PROVIDER_NAME } from './tools/auth.js';
28
28
  import { V2_AGENT_KEYS, conflictMessage, conflictsOf, toEngineAgent } from './agentDialect.js';
29
+ import { MCP_TRANSPORTS } from './tools/mcpTransports.js';
29
30
  // ── Leaf rules ───────────────────────────────────────────────────────────────
30
31
  /**
31
32
  * ADK's agent-name rule (validateAgentName in ADK's base_agent), kept: checked here
@@ -217,8 +218,12 @@ const oauth2Auth = z
217
218
  grant: z
218
219
  .enum(['authorization_code', 'client_credentials'])
219
220
  .describe('authorization_code: each user\'s own token, granted through the consent pause (ADR 0085). client_credentials: the server\'s own token, from the token endpoint.'),
220
- authorization_url: z.string().url().optional().describe('The provider\'s authorization endpoint (https). authorization_code only.'),
221
- token_url: z.string().url().describe('The provider\'s token endpoint (https).'),
221
+ client_registration: z
222
+ .literal('dynamic')
223
+ .optional()
224
+ .describe('"dynamic": an MCP server\'s authorization_code grant with no pre-issued client. The authorization server is discovered from the MCP server (RFC 9728, RFC 8414) and this deployment registers itself as a public PKCE client (RFC 7591), once per server and redirect URI, sealed in the credential store (ADR 0124). Every discovered host must be on the operator\'s OAuth host allowlist. MCP servers only; no client id, client secret or endpoint URL beside it.'),
225
+ authorization_url: z.string().url().optional().describe('The provider\'s authorization endpoint (https). authorization_code only; discovered with client_registration: dynamic.'),
226
+ token_url: z.string().url().optional().describe('The provider\'s token endpoint (https). Required, except with client_registration: dynamic, which discovers it.'),
222
227
  client_id: z.string().min(1).max(512).optional().describe('The OAuth client id, written out (it is not a secret). Or client_id_env.'),
223
228
  client_id_env: envName.optional().describe('Environment variable holding the client id. Or client_id.'),
224
229
  client_secret_env: envName
@@ -228,13 +233,25 @@ const oauth2Auth = z
228
233
  authorization_params: z
229
234
  .record(z.string().regex(/^[A-Za-z0-9_.-]{1,64}$/), z.string().max(512))
230
235
  .optional()
231
- .describe('Extra authorization parameters (e.g. access_type: offline). authorization_code only; never state, redirect_uri, scope, client_id or the PKCE pair.'),
236
+ .describe('Extra authorization parameters (e.g. access_type: offline). authorization_code only; never state, redirect_uri, scope, client_id, resource or the PKCE pair.'),
232
237
  })
233
238
  .superRefine((o, ctx) => {
234
- if (!!o.client_id === !!o.client_id_env)
235
- ctx.addIssue({ code: 'custom', path: ['client_id'], message: 'exactly one of client_id or client_id_env' });
236
- if (o.grant === 'authorization_code' && !o.authorization_url) {
237
- ctx.addIssue({ code: 'custom', path: ['authorization_url'], message: 'an authorization_code grant needs authorization_url' });
239
+ if (o.client_registration === 'dynamic') {
240
+ if (o.grant !== 'authorization_code')
241
+ ctx.addIssue({ code: 'custom', path: ['client_registration'], message: 'client_registration: dynamic is for an authorization_code grant' });
242
+ for (const key of ['client_id', 'client_id_env', 'client_secret_env', 'authorization_url', 'token_url']) {
243
+ if (o[key] !== undefined)
244
+ ctx.addIssue({ code: 'custom', path: [key], message: `${key} cannot be combined with client_registration: dynamic, which discovers the endpoints and registers the client (ADR 0124)` });
245
+ }
246
+ }
247
+ else {
248
+ if (!!o.client_id === !!o.client_id_env)
249
+ ctx.addIssue({ code: 'custom', path: ['client_id'], message: 'exactly one of client_id or client_id_env' });
250
+ if (o.token_url === undefined)
251
+ ctx.addIssue({ code: 'custom', path: ['token_url'], message: 'token_url is required (on an MCP server, client_registration: dynamic discovers it)' });
252
+ if (o.grant === 'authorization_code' && !o.authorization_url) {
253
+ ctx.addIssue({ code: 'custom', path: ['authorization_url'], message: 'an authorization_code grant needs authorization_url' });
254
+ }
238
255
  }
239
256
  if (o.grant === 'client_credentials') {
240
257
  if (!o.client_secret_env)
@@ -244,13 +261,37 @@ const oauth2Auth = z
244
261
  ctx.addIssue({ code: 'custom', path: [key], message: `${key} is for authorization_code only` });
245
262
  }
246
263
  }
247
- const reserved = ['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope'];
264
+ const reserved = ['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope', 'resource'];
248
265
  for (const key of Object.keys(o.authorization_params ?? {})) {
249
266
  if (reserved.includes(key))
250
267
  ctx.addIssue({ code: 'custom', path: ['authorization_params', key], message: `authorization_params may not set "${key}"` });
251
268
  }
252
269
  })
253
270
  .describe('An OAuth 2 access token sent as a bearer token on every call (lib/tools/oauthTools.ts). Secrets come from environment variable names, never values.');
271
+ // ── MCP (ADR 0124) ───────────────────────────────────────────────────────────
272
+ const mcpTransport = z
273
+ .enum(MCP_TRANSPORTS)
274
+ .describe('How the MCP server is reached: auto (the default) tries Streamable HTTP and falls back to the legacy SSE transport on the spec\'s signal; streamable_http or sse use that one only (ADR 0124).');
275
+ /** An `mcp_servers` entry's name: lowercase, bounded, one character class (no backtracking). */
276
+ export const MCP_SERVER_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
277
+ const mcpServerEntry = z
278
+ .strictObject({
279
+ name: z
280
+ .string()
281
+ .regex(MCP_SERVER_NAME, 'lowercase letters, digits, _ and -, starting with a letter (at most 64)')
282
+ .describe('A short name for the server, unique on this agent: logs and errors name it.'),
283
+ url: z.string().url().describe('The MCP server\'s URL: its Streamable HTTP endpoint, or a legacy SSE endpoint. Checked by the SSRF guard.'),
284
+ tools: z
285
+ .array(z.string().min(1))
286
+ .min(1)
287
+ .describe('The server\'s tools this agent may use, by name; any other tool it lists is not exposed. A name may appear on one server only, and never among the agent\'s own tools. require_approval may name these.'),
288
+ auth: z
289
+ .strictObject({ oauth2: oauth2Auth })
290
+ .optional()
291
+ .describe('The OAuth grant this server takes (ADR 0112, ADR 0124). Without it, MCP_BEARER_TOKENS applies.'),
292
+ transport: mcpTransport.optional(),
293
+ })
294
+ .describe('One MCP server among several (ADR 0124).');
254
295
  // ── OpenAPI ──────────────────────────────────────────────────────────────────
255
296
  const openapiEntry = z
256
297
  .strictObject({
@@ -326,7 +367,7 @@ const agentFields = {
326
367
  mcp_server_url: z
327
368
  .string()
328
369
  .optional()
329
- .describe('MCP server (SSE) whose tools are discovered at runtime and merged with `tools`.'),
370
+ .describe('MCP server (Streamable HTTP, or legacy SSE) whose tools are discovered at runtime and merged with `tools`. For more than one server, use mcp_servers.'),
330
371
  mcp_tools: z
331
372
  .array(z.string().min(1))
332
373
  .min(1)
@@ -336,6 +377,13 @@ const agentFields = {
336
377
  .strictObject({ oauth2: oauth2Auth })
337
378
  .optional()
338
379
  .describe('The OAuth grant the MCP server takes (ADR 0112). With authorization_code, each user\'s own token on their own connection, and mcp_tools is required. Needs mcp_server_url; replaces MCP_BEARER_TOKENS for this server.'),
380
+ mcp_transport: mcpTransport.optional().describe('How mcp_server_url is reached: auto (the default), streamable_http or sse (ADR 0124). Needs mcp_server_url.'),
381
+ mcp_servers: z
382
+ .array(mcpServerEntry)
383
+ .min(1)
384
+ .max(16)
385
+ .optional()
386
+ .describe('Several MCP servers, each with the tools this agent may use from it, its grant and its transport (ADR 0124). Cannot be combined with mcp_server_url.'),
339
387
  skills: skillsSchema.optional(),
340
388
  openapi: z.array(openapiEntry).min(1).optional(),
341
389
  code_execution: z
@@ -678,6 +726,61 @@ export const REASONING_OLDER_SPELLING = ['thinkingConfig', 'reasoningEffort'];
678
726
  const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
679
727
  /** Same normalisation lib/dispatch.ts uses to match a route to a subagent. */
680
728
  const routeNorm = (s) => s.replace(/[^a-z0-9]/gi, '').toLowerCase();
729
+ /** Every tool name an agent's `mcp_servers` entries list. */
730
+ function mcpServerTools(agent) {
731
+ return (Array.isArray(agent.mcp_servers) ? agent.mcp_servers : []).flatMap((e) => (isObj(e) && Array.isArray(e.tools) ? e.tools.filter((t) => typeof t === 'string') : []));
732
+ }
733
+ /**
734
+ * An agent's `mcp_servers` against itself and its other keys (ADR 0124):
735
+ * not beside `mcp_server_url`; names unique; a tool name on one server only
736
+ * and never among the agent's own `tools` or OpenAPI `operations`, so no
737
+ * server can shadow another's tool or the agent's own; and dynamic client
738
+ * registration only here or under `mcp_auth`, never on an OpenAPI entry.
739
+ */
740
+ function mcpServersProblems(agent, path) {
741
+ const out = [];
742
+ (Array.isArray(agent.openapi) ? agent.openapi : []).forEach((e, i) => {
743
+ if (isObj(e) && isObj(e.auth) && isObj(e.auth.oauth2) && e.auth.oauth2.client_registration !== undefined) {
744
+ out.push({ path: [...path, 'openapi', i, 'auth', 'oauth2', 'client_registration'], message: 'client_registration: dynamic discovers its endpoints from an MCP server; an OpenAPI entry names its own (ADR 0124)' });
745
+ }
746
+ });
747
+ if (!Array.isArray(agent.mcp_servers))
748
+ return out;
749
+ if (agent.mcp_server_url !== undefined) {
750
+ out.push({ path: [...path, 'mcp_servers'], message: 'cannot be combined with mcp_server_url: list that server as one more mcp_servers entry (ADR 0124)' });
751
+ }
752
+ const own = new Set([
753
+ ...(Array.isArray(agent.tools) ? agent.tools.filter((t) => typeof t === 'string') : []),
754
+ ...(Array.isArray(agent.openapi) ? agent.openapi : []).flatMap((e) => (isObj(e) && Array.isArray(e.operations) ? e.operations.filter((t) => typeof t === 'string') : [])),
755
+ ]);
756
+ const names = new Map();
757
+ const owner = new Map();
758
+ agent.mcp_servers.forEach((entry, i) => {
759
+ if (!isObj(entry))
760
+ return;
761
+ const name = typeof entry.name === 'string' ? entry.name : `#${i}`;
762
+ if (typeof entry.name === 'string') {
763
+ if (names.has(entry.name))
764
+ out.push({ path: [...path, 'mcp_servers', i, 'name'], message: `'${entry.name}' names mcp_servers entry ${names.get(entry.name)} too; each server's name is unique on the agent` });
765
+ else
766
+ names.set(entry.name, i);
767
+ }
768
+ (Array.isArray(entry.tools) ? entry.tools : []).forEach((tool, j) => {
769
+ if (typeof tool !== 'string')
770
+ return;
771
+ if (own.has(tool)) {
772
+ out.push({ path: [...path, 'mcp_servers', i, 'tools', j], message: `'${tool}' is also one of this agent's own tools; an MCP server's tool may not share a name with it (ADR 0124)` });
773
+ }
774
+ else if (owner.has(tool) && owner.get(tool) !== name) {
775
+ out.push({ path: [...path, 'mcp_servers', i, 'tools', j], message: `'${tool}' is also listed on MCP server '${owner.get(tool)}'; a tool name may come from one server only (ADR 0124)` });
776
+ }
777
+ else {
778
+ owner.set(tool, name);
779
+ }
780
+ });
781
+ });
782
+ return out;
783
+ }
681
784
  /**
682
785
  * Rules that span fields, which a JSON Schema an editor reads cannot express
683
786
  * cleanly. Defensive about shape: runs on raw input alongside zod, so a file
@@ -713,10 +816,10 @@ function crossFieldProblems(raw) {
713
816
  // An OpenAPI operation can be gated once it is named under `operations`,
714
817
  // and an MCP tool once it is named under `mcp_tools`.
715
818
  const operations = (Array.isArray(agent.openapi) ? agent.openapi : []).flatMap((e) => (isObj(e) && Array.isArray(e.operations) ? e.operations : []));
716
- const mcpTools = Array.isArray(agent.mcp_tools) ? agent.mcp_tools : [];
819
+ const mcpTools = [...(Array.isArray(agent.mcp_tools) ? agent.mcp_tools : []), ...mcpServerTools(agent)];
717
820
  (Array.isArray(agent.require_approval) ? agent.require_approval : []).forEach((name, j) => {
718
821
  if (typeof name === 'string' && !tools.includes(name) && !operations.includes(name) && !mcpTools.includes(name)) {
719
- out.push({ path: [...path, 'require_approval', j], message: `'${name}' is not in this agent's tools, its openapi operations or its mcp_tools` });
822
+ out.push({ path: [...path, 'require_approval', j], message: `'${name}' is not in this agent's tools, its openapi operations or its mcp_tools (or an mcp_servers entry's tools)` });
720
823
  }
721
824
  });
722
825
  };
@@ -772,6 +875,10 @@ function crossFieldProblems(raw) {
772
875
  if (isObj(agent.mcp_auth) && isObj(agent.mcp_auth.oauth2) && agent.mcp_auth.oauth2.grant === 'authorization_code' && agent.mcp_tools === undefined) {
773
876
  out.push({ path: [...path, 'mcp_auth'], message: 'an authorization_code mcp_auth needs mcp_tools: no user\'s grant exists at startup to list the server\'s tools' });
774
877
  }
878
+ if (agent.mcp_transport !== undefined && typeof agent.mcp_server_url !== 'string') {
879
+ out.push({ path: [...path, 'mcp_transport'], message: 'mcp_transport is how mcp_server_url is reached; it needs mcp_server_url (an mcp_servers entry names its own transport)' });
880
+ }
881
+ out.push(...mcpServersProblems(agent, path));
775
882
  };
776
883
  if (isObj(raw.orchestrator))
777
884
  executionProblems(raw.orchestrator, ['orchestrator'], true);
@@ -36,7 +36,7 @@ export interface CredentialUse {
36
36
  }
37
37
  /** The parts of an `oauth2` block read here. */
38
38
  interface OAuth2Like {
39
- token_url: string;
39
+ token_url?: string;
40
40
  client_secret_env?: string;
41
41
  }
42
42
  /** The parts of an `openapi:` entry read here. */
@@ -58,6 +58,12 @@ interface AgentLike {
58
58
  mcp_auth?: {
59
59
  oauth2?: OAuth2Like;
60
60
  };
61
+ mcp_servers?: Array<{
62
+ url?: string;
63
+ auth?: {
64
+ oauth2?: OAuth2Like;
65
+ };
66
+ }>;
61
67
  yaml_reference?: string;
62
68
  }
63
69
  /** The parts of a syndicate config read here. */
@@ -16,6 +16,8 @@
16
16
  import { resolve } from 'node:path';
17
17
  import { credentialHostProblem, credentialHosts } from './credentialHosts.js';
18
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] : []);
19
21
  /** The credential variables one agent's tools send. */
20
22
  export function agentCredentialUses(agent, prefix = '') {
21
23
  const name = `${prefix}${agent.name ?? ''}`;
@@ -31,11 +33,17 @@ export function agentCredentialUses(agent, prefix = '') {
31
33
  if (auth.api_key?.env)
32
34
  out.push({ agent: name, tools, env: auth.api_key.env, role: 'api_key', ...servers, openapi: entry });
33
35
  if (auth.oauth2?.client_secret_env)
34
- out.push({ agent: name, tools, env: auth.oauth2.client_secret_env, role: 'client_secret_env', destinations: [auth.oauth2.token_url] });
36
+ out.push({ agent: name, tools, env: auth.oauth2.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(auth.oauth2) });
35
37
  }
36
38
  const mcp = agent.mcp_auth?.oauth2;
37
39
  if (mcp?.client_secret_env)
38
- out.push({ agent: name, tools: `mcp ${agent.mcp_server_url ?? ''}`, env: mcp.client_secret_env, role: 'client_secret_env', destinations: [mcp.token_url] });
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
+ }
39
47
  return out;
40
48
  }
41
49
  /**
@@ -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
@@ -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;