@contractspec/lib.ai-agent 2.5.0 → 2.7.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 (39) hide show
  1. package/README.md +33 -2
  2. package/dist/agent/agent-factory.d.ts +5 -0
  3. package/dist/agent/agent-factory.js +221 -11
  4. package/dist/agent/contract-spec-agent.d.ts +8 -0
  5. package/dist/agent/contract-spec-agent.js +210 -10
  6. package/dist/agent/index.js +334 -39
  7. package/dist/agent/json-runner.js +210 -10
  8. package/dist/agent/unified-agent.d.ts +4 -0
  9. package/dist/agent/unified-agent.js +334 -39
  10. package/dist/exporters/claude-agent-exporter.d.ts +1 -0
  11. package/dist/exporters/claude-agent-exporter.js +11 -1
  12. package/dist/exporters/index.js +11 -1
  13. package/dist/exporters/types.d.ts +3 -10
  14. package/dist/node/agent/agent-factory.js +221 -11
  15. package/dist/node/agent/contract-spec-agent.js +210 -10
  16. package/dist/node/agent/index.js +334 -39
  17. package/dist/node/agent/json-runner.js +210 -10
  18. package/dist/node/agent/unified-agent.js +334 -39
  19. package/dist/node/exporters/claude-agent-exporter.js +11 -1
  20. package/dist/node/exporters/index.js +11 -1
  21. package/dist/node/providers/claude-agent-sdk/adapter.js +260 -23
  22. package/dist/node/providers/claude-agent-sdk/index.js +260 -23
  23. package/dist/node/providers/index.js +260 -23
  24. package/dist/node/tools/index.js +154 -18
  25. package/dist/node/tools/mcp-client-helpers.js +106 -0
  26. package/dist/node/tools/mcp-client.js +155 -18
  27. package/dist/providers/claude-agent-sdk/adapter.d.ts +4 -0
  28. package/dist/providers/claude-agent-sdk/adapter.js +260 -23
  29. package/dist/providers/claude-agent-sdk/index.d.ts +8 -0
  30. package/dist/providers/claude-agent-sdk/index.js +260 -23
  31. package/dist/providers/index.js +260 -23
  32. package/dist/providers/types.d.ts +1 -1
  33. package/dist/tools/index.js +154 -18
  34. package/dist/tools/mcp-client-helpers.d.ts +12 -0
  35. package/dist/tools/mcp-client-helpers.js +106 -0
  36. package/dist/tools/mcp-client.d.ts +55 -3
  37. package/dist/tools/mcp-client.js +155 -18
  38. package/dist/tools/mcp-client.test.d.ts +1 -0
  39. package/package.json +24 -12
package/README.md CHANGED
@@ -4,10 +4,8 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dt/@contractspec/lib.ai-agent)](https://www.npmjs.com/package/@contractspec/lib.ai-agent)
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lssm-tech/contractspec)
6
6
 
7
-
8
7
  Website: https://contractspec.io/
9
8
 
10
-
11
9
  **AI governance for ContractSpec** — Constrain what AI agents can change, enforce contracts they must respect.
12
10
 
13
11
  Stateful AI agent orchestration with type-safe specs, tool execution, knowledge bindings, and per-tenant guardrails. Agents read contracts as their source of truth, enabling triage, growth experiments, and DevOps automation with contract-enforced safety.
@@ -19,6 +17,7 @@ Stateful AI agent orchestration with type-safe specs, tool execution, knowledge
19
17
  - Memory framework that mixes working memory with long-term persistence hooks
20
18
  - Tool registry/executor with structured input validation and telemetry hooks
21
19
  - Approval workflow helpers for human-in-the-loop gates (see `/approval`)
20
+ - MCP client support for `stdio`, `sse`, and `http` transports
22
21
 
23
22
  ## Quickstart
24
23
 
@@ -47,3 +46,35 @@ if (result.requiresEscalation) notifyHuman(result);
47
46
  ```
48
47
 
49
48
  See `examples/ai-support-bot` for a full workflow including ticket ingestion and approval queues.
49
+
50
+ ## MCP Client Tooling
51
+
52
+ ```ts
53
+ import { createAgentFactory } from '@contractspec/lib.ai-agent/agent/agent-factory';
54
+
55
+ const factory = createAgentFactory({
56
+ defaultModel,
57
+ registry,
58
+ toolHandlers,
59
+ mcpServers: [
60
+ {
61
+ name: 'filesystem',
62
+ command: 'npx',
63
+ args: ['-y', '@modelcontextprotocol/server-filesystem', '/workspace'],
64
+ toolPrefix: 'fs',
65
+ },
66
+ {
67
+ name: 'posthog',
68
+ transport: 'http',
69
+ url: process.env.POSTHOG_MCP_URL!,
70
+ accessTokenEnvVar: 'POSTHOG_MCP_TOKEN',
71
+ toolPrefix: 'ph',
72
+ },
73
+ ],
74
+ });
75
+
76
+ const agent = await factory.create('support.bot');
77
+ const result = await agent.generate({ prompt: 'Summarize today\'s incidents.' });
78
+
79
+ await agent.cleanup();
80
+ ```
@@ -3,6 +3,7 @@ import type { KnowledgeRetriever } from '@contractspec/lib.knowledge/retriever';
3
3
  import type { AgentSpec } from '../spec/spec';
4
4
  import type { AgentRegistry } from '../spec/registry';
5
5
  import type { ToolHandler } from '../types';
6
+ import type { McpClientConfig } from '../tools/mcp-client';
6
7
  import type { AgentSessionStore } from '../session/store';
7
8
  import type { TelemetryCollector } from '../telemetry/adapter';
8
9
  import type { PostHogLLMConfig, PostHogTracingOptions } from '../telemetry/posthog-types';
@@ -29,6 +30,8 @@ export interface AgentFactoryConfig {
29
30
  };
30
31
  /** Additional tools to provide to all agents */
31
32
  additionalTools?: Record<string, Tool<unknown, unknown>>;
33
+ /** MCP servers to provide to all agents */
34
+ mcpServers?: McpClientConfig[];
32
35
  }
33
36
  /**
34
37
  * Options for creating an agent instance.
@@ -40,6 +43,8 @@ export interface CreateAgentOptions {
40
43
  toolHandlers?: Map<string, ToolHandler>;
41
44
  /** Additional tools for this instance */
42
45
  additionalTools?: Record<string, Tool<unknown, unknown>>;
46
+ /** MCP servers for this instance */
47
+ mcpServers?: McpClientConfig[];
43
48
  }
44
49
  /**
45
50
  * Factory for creating ContractSpec agents from specs.
@@ -2400,6 +2400,171 @@ var init_knowledge_tool = __esm(() => {
2400
2400
  init_i18n();
2401
2401
  });
2402
2402
 
2403
+ // src/tools/mcp-client-helpers.ts
2404
+ import {
2405
+ Experimental_StdioMCPTransport as StdioClientTransport
2406
+ } from "@ai-sdk/mcp/mcp-stdio";
2407
+ function buildMcpTransport(config) {
2408
+ const transport = resolveTransportType(config);
2409
+ if (transport === "stdio") {
2410
+ const stdioConfig = resolveStdioConfig(config);
2411
+ return new StdioClientTransport(stdioConfig);
2412
+ }
2413
+ const remoteConfig = config;
2414
+ const headers = resolveRemoteHeaders(remoteConfig);
2415
+ const remoteTransport = {
2416
+ type: transport,
2417
+ url: requireNonEmptyString(remoteConfig.url, "url", config.name)
2418
+ };
2419
+ if (headers) {
2420
+ remoteTransport.headers = headers;
2421
+ }
2422
+ if (remoteConfig.authProvider) {
2423
+ remoteTransport.authProvider = remoteConfig.authProvider;
2424
+ }
2425
+ return remoteTransport;
2426
+ }
2427
+ function prefixToolNames(config, tools) {
2428
+ const prefix = config.toolPrefix?.trim();
2429
+ if (!prefix) {
2430
+ return tools;
2431
+ }
2432
+ const prefixedTools = {};
2433
+ for (const [toolName, tool3] of Object.entries(tools)) {
2434
+ prefixedTools[`${prefix}_${toolName}`] = tool3;
2435
+ }
2436
+ return prefixedTools;
2437
+ }
2438
+ function getErrorMessage(error) {
2439
+ if (error instanceof Error) {
2440
+ return error.message;
2441
+ }
2442
+ return String(error);
2443
+ }
2444
+ function resolveTransportType(config) {
2445
+ return config.transport ?? "stdio";
2446
+ }
2447
+ function resolveStdioConfig(config) {
2448
+ const stdioConfig = config;
2449
+ return {
2450
+ command: requireNonEmptyString(stdioConfig.command, "command", config.name),
2451
+ args: stdioConfig.args,
2452
+ env: stdioConfig.env,
2453
+ cwd: stdioConfig.cwd
2454
+ };
2455
+ }
2456
+ function resolveRemoteHeaders(config) {
2457
+ const headers = {
2458
+ ...config.headers ?? {}
2459
+ };
2460
+ const accessToken = config.accessToken ?? resolveEnvToken(config.accessTokenEnvVar);
2461
+ if (accessToken && headers.Authorization === undefined) {
2462
+ headers.Authorization = `Bearer ${accessToken}`;
2463
+ }
2464
+ return Object.keys(headers).length > 0 ? headers : undefined;
2465
+ }
2466
+ function resolveEnvToken(envVarName) {
2467
+ if (!envVarName) {
2468
+ return;
2469
+ }
2470
+ const value = process.env[envVarName];
2471
+ if (!value) {
2472
+ return;
2473
+ }
2474
+ const trimmed = value.trim();
2475
+ return trimmed.length > 0 ? trimmed : undefined;
2476
+ }
2477
+ function requireNonEmptyString(value, field, serverName) {
2478
+ if (!value) {
2479
+ throw new Error(`MCP server "${serverName}" is missing required "${field}".`);
2480
+ }
2481
+ const trimmed = value.trim();
2482
+ if (trimmed.length === 0) {
2483
+ throw new Error(`MCP server "${serverName}" has an empty "${field}".`);
2484
+ }
2485
+ return trimmed;
2486
+ }
2487
+ var init_mcp_client_helpers = () => {};
2488
+
2489
+ // src/tools/mcp-client.ts
2490
+ import {
2491
+ experimental_createMCPClient
2492
+ } from "@ai-sdk/mcp";
2493
+ async function mcpServerToTools(config) {
2494
+ let client = null;
2495
+ try {
2496
+ const transport = buildMcpTransport(config);
2497
+ client = await experimental_createMCPClient({
2498
+ transport,
2499
+ name: config.clientName,
2500
+ version: config.clientVersion
2501
+ });
2502
+ const tools = await client.tools();
2503
+ const prefixedTools = prefixToolNames(config, tools);
2504
+ const connectedClient = client;
2505
+ return {
2506
+ tools: prefixedTools,
2507
+ cleanup: () => connectedClient.close(),
2508
+ serverToolNames: {
2509
+ [config.name]: Object.keys(prefixedTools)
2510
+ }
2511
+ };
2512
+ } catch (error) {
2513
+ if (client) {
2514
+ await client.close().catch(() => {
2515
+ return;
2516
+ });
2517
+ }
2518
+ throw new Error(`[MCP:${config.name}] Failed to connect tools: ${getErrorMessage(error)}`);
2519
+ }
2520
+ }
2521
+ async function createMcpToolsets(configs, options = {}) {
2522
+ const connected = [];
2523
+ try {
2524
+ for (const config of configs) {
2525
+ const result = await mcpServerToTools(config);
2526
+ connected.push(result);
2527
+ }
2528
+ } catch (error) {
2529
+ await Promise.allSettled(connected.map((result) => result.cleanup()));
2530
+ throw error;
2531
+ }
2532
+ const combinedTools = {};
2533
+ const serverToolNames = {};
2534
+ const collisionStrategy = options.onNameCollision ?? "overwrite";
2535
+ try {
2536
+ for (const result of connected) {
2537
+ for (const [serverName, toolNames] of Object.entries(result.serverToolNames)) {
2538
+ serverToolNames[serverName] = toolNames;
2539
+ }
2540
+ for (const [toolName, tool3] of Object.entries(result.tools)) {
2541
+ const hasCollision = combinedTools[toolName] !== undefined;
2542
+ if (hasCollision && collisionStrategy === "error") {
2543
+ throw new Error(`Duplicate MCP tool name "${toolName}" detected. Use "toolPrefix" or set onNameCollision to "overwrite".`);
2544
+ }
2545
+ combinedTools[toolName] = tool3;
2546
+ }
2547
+ }
2548
+ } catch (error) {
2549
+ await Promise.allSettled(connected.map((result) => result.cleanup()));
2550
+ throw error;
2551
+ }
2552
+ return {
2553
+ tools: combinedTools,
2554
+ serverToolNames,
2555
+ cleanup: async () => {
2556
+ const cleanupResults = await Promise.allSettled(connected.map((result) => result.cleanup()));
2557
+ const failures = cleanupResults.filter((result) => result.status === "rejected");
2558
+ if (failures.length > 0) {
2559
+ throw new Error(`Failed to cleanup ${failures.length} MCP client connection(s).`);
2560
+ }
2561
+ }
2562
+ };
2563
+ }
2564
+ var init_mcp_client = __esm(() => {
2565
+ init_mcp_client_helpers();
2566
+ });
2567
+
2403
2568
  // src/knowledge/injector.ts
2404
2569
  async function injectStaticKnowledge(instructions, knowledgeRefs, retriever, locale) {
2405
2570
  if (!retriever)
@@ -2909,25 +3074,59 @@ class ContractSpecAgent {
2909
3074
  tools;
2910
3075
  config;
2911
3076
  instructions;
3077
+ mcpCleanup;
2912
3078
  activeStepContexts = new Map;
2913
- constructor(config, instructions, tools) {
3079
+ constructor(config, instructions, tools, mcpCleanup) {
2914
3080
  this.config = config;
2915
3081
  this.spec = config.spec;
2916
3082
  this.id = agentKey(config.spec.meta);
2917
3083
  this.tools = tools;
2918
3084
  this.instructions = instructions;
3085
+ this.mcpCleanup = mcpCleanup;
2919
3086
  }
2920
3087
  static async create(config) {
2921
3088
  const effectiveConfig = config;
2922
- const instructions = await injectStaticKnowledge(effectiveConfig.spec.instructions, effectiveConfig.spec.knowledge ?? [], effectiveConfig.knowledgeRetriever);
2923
- const specTools = specToolsToAISDKTools(effectiveConfig.spec.tools, effectiveConfig.toolHandlers, { agentId: agentKey(effectiveConfig.spec.meta) });
2924
- const knowledgeTool = effectiveConfig.knowledgeRetriever ? createKnowledgeQueryTool(effectiveConfig.knowledgeRetriever, effectiveConfig.spec.knowledge ?? []) : null;
2925
- const tools = {
2926
- ...specTools,
2927
- ...knowledgeTool ? { query_knowledge: knowledgeTool } : {},
2928
- ...effectiveConfig.additionalTools ?? {}
2929
- };
2930
- return new ContractSpecAgent(effectiveConfig, instructions, tools);
3089
+ let mcpToolset = null;
3090
+ if ((effectiveConfig.mcpServers?.length ?? 0) > 0) {
3091
+ mcpToolset = await createMcpToolsets(effectiveConfig.mcpServers ?? [], {
3092
+ onNameCollision: "error"
3093
+ });
3094
+ }
3095
+ try {
3096
+ const instructions = await injectStaticKnowledge(effectiveConfig.spec.instructions, effectiveConfig.spec.knowledge ?? [], effectiveConfig.knowledgeRetriever);
3097
+ const specTools = specToolsToAISDKTools(effectiveConfig.spec.tools, effectiveConfig.toolHandlers, { agentId: agentKey(effectiveConfig.spec.meta) });
3098
+ const knowledgeTool = effectiveConfig.knowledgeRetriever ? createKnowledgeQueryTool(effectiveConfig.knowledgeRetriever, effectiveConfig.spec.knowledge ?? []) : null;
3099
+ const reservedToolNames = new Set(Object.keys(specTools));
3100
+ if (knowledgeTool) {
3101
+ reservedToolNames.add("query_knowledge");
3102
+ }
3103
+ const conflictingMcpTools = Object.keys(mcpToolset?.tools ?? {}).filter((toolName) => reservedToolNames.has(toolName));
3104
+ if (conflictingMcpTools.length > 0) {
3105
+ throw new Error(`MCP tools conflict with agent tools: ${conflictingMcpTools.join(", ")}. Configure MCP toolPrefix values to avoid collisions.`);
3106
+ }
3107
+ const tools = {
3108
+ ...specTools,
3109
+ ...knowledgeTool ? { query_knowledge: knowledgeTool } : {},
3110
+ ...mcpToolset?.tools ?? {},
3111
+ ...effectiveConfig.additionalTools ?? {}
3112
+ };
3113
+ return new ContractSpecAgent(effectiveConfig, instructions, tools, mcpToolset?.cleanup);
3114
+ } catch (error) {
3115
+ if (mcpToolset) {
3116
+ await mcpToolset.cleanup().catch(() => {
3117
+ return;
3118
+ });
3119
+ }
3120
+ throw error;
3121
+ }
3122
+ }
3123
+ async cleanup() {
3124
+ if (!this.mcpCleanup) {
3125
+ return;
3126
+ }
3127
+ const cleanup = this.mcpCleanup;
3128
+ this.mcpCleanup = undefined;
3129
+ await cleanup();
2931
3130
  }
2932
3131
  async generate(params) {
2933
3132
  const sessionId = params.options?.sessionId ?? generateSessionId();
@@ -3102,6 +3301,7 @@ var init_contract_spec_agent = __esm(() => {
3102
3301
  init_spec();
3103
3302
  init_tool_adapter();
3104
3303
  init_knowledge_tool();
3304
+ init_mcp_client();
3105
3305
  init_injector();
3106
3306
  init_adapter();
3107
3307
  ContractSpecCallOptionsSchema = z3.object({
@@ -3136,6 +3336,10 @@ class AgentFactory {
3136
3336
  ...this.config.additionalTools,
3137
3337
  ...options?.additionalTools
3138
3338
  };
3339
+ const mergedMcpServers = [
3340
+ ...this.config.mcpServers ?? [],
3341
+ ...options?.mcpServers ?? []
3342
+ ];
3139
3343
  return ContractSpecAgent.create({
3140
3344
  spec,
3141
3345
  model: options?.model ?? this.config.defaultModel,
@@ -3144,7 +3348,8 @@ class AgentFactory {
3144
3348
  sessionStore: this.config.sessionStore,
3145
3349
  telemetryCollector: this.config.telemetryCollector,
3146
3350
  posthogConfig: this.config.posthogConfig,
3147
- additionalTools: mergedTools
3351
+ additionalTools: mergedTools,
3352
+ mcpServers: mergedMcpServers.length > 0 ? mergedMcpServers : undefined
3148
3353
  });
3149
3354
  }
3150
3355
  async getOrCreate(name, version) {
@@ -3158,6 +3363,11 @@ class AgentFactory {
3158
3363
  return agent;
3159
3364
  }
3160
3365
  clearCache() {
3366
+ for (const agent of this.cache.values()) {
3367
+ agent.cleanup().catch(() => {
3368
+ return;
3369
+ });
3370
+ }
3161
3371
  this.cache.clear();
3162
3372
  }
3163
3373
  listAvailable() {
@@ -2,6 +2,7 @@ import { type LanguageModel, type Tool, type ToolSet } from 'ai';
2
2
  import type { KnowledgeRetriever } from '@contractspec/lib.knowledge/retriever';
3
3
  import type { AgentSpec } from '../spec/spec';
4
4
  import type { AgentGenerateParams, AgentGenerateResult, AgentStreamParams, ToolHandler } from '../types';
5
+ import { type McpClientConfig } from '../tools/mcp-client';
5
6
  import { type AgentSessionStore } from '../session/store';
6
7
  import { type TelemetryCollector } from '../telemetry/adapter';
7
8
  import type { PostHogLLMConfig, PostHogTracingOptions } from '../telemetry/posthog-types';
@@ -32,6 +33,8 @@ export interface ContractSpecAgentConfig {
32
33
  };
33
34
  /** Additional AI SDK tools (e.g., from MCP servers) */
34
35
  additionalTools?: Record<string, ExecutableTool>;
36
+ /** MCP servers to connect and expose as tools */
37
+ mcpServers?: McpClientConfig[];
35
38
  }
36
39
  /**
37
40
  * ContractSpec Agent implementation using AI SDK v6.
@@ -51,6 +54,7 @@ export declare class ContractSpecAgent {
51
54
  readonly tools: Record<string, ExecutableTool>;
52
55
  private readonly config;
53
56
  private instructions;
57
+ private mcpCleanup?;
54
58
  private readonly activeStepContexts;
55
59
  private constructor();
56
60
  /**
@@ -58,6 +62,10 @@ export declare class ContractSpecAgent {
58
62
  * This is async because knowledge injection may need to fetch static content.
59
63
  */
60
64
  static create(config: ContractSpecAgentConfig): Promise<ContractSpecAgent>;
65
+ /**
66
+ * Cleanup resources held by this agent (e.g., MCP connections).
67
+ */
68
+ cleanup(): Promise<void>;
61
69
  /**
62
70
  * Generate a response (non-streaming).
63
71
  */
@@ -2400,6 +2400,171 @@ var init_knowledge_tool = __esm(() => {
2400
2400
  init_i18n();
2401
2401
  });
2402
2402
 
2403
+ // src/tools/mcp-client-helpers.ts
2404
+ import {
2405
+ Experimental_StdioMCPTransport as StdioClientTransport
2406
+ } from "@ai-sdk/mcp/mcp-stdio";
2407
+ function buildMcpTransport(config) {
2408
+ const transport = resolveTransportType(config);
2409
+ if (transport === "stdio") {
2410
+ const stdioConfig = resolveStdioConfig(config);
2411
+ return new StdioClientTransport(stdioConfig);
2412
+ }
2413
+ const remoteConfig = config;
2414
+ const headers = resolveRemoteHeaders(remoteConfig);
2415
+ const remoteTransport = {
2416
+ type: transport,
2417
+ url: requireNonEmptyString(remoteConfig.url, "url", config.name)
2418
+ };
2419
+ if (headers) {
2420
+ remoteTransport.headers = headers;
2421
+ }
2422
+ if (remoteConfig.authProvider) {
2423
+ remoteTransport.authProvider = remoteConfig.authProvider;
2424
+ }
2425
+ return remoteTransport;
2426
+ }
2427
+ function prefixToolNames(config, tools) {
2428
+ const prefix = config.toolPrefix?.trim();
2429
+ if (!prefix) {
2430
+ return tools;
2431
+ }
2432
+ const prefixedTools = {};
2433
+ for (const [toolName, tool3] of Object.entries(tools)) {
2434
+ prefixedTools[`${prefix}_${toolName}`] = tool3;
2435
+ }
2436
+ return prefixedTools;
2437
+ }
2438
+ function getErrorMessage(error) {
2439
+ if (error instanceof Error) {
2440
+ return error.message;
2441
+ }
2442
+ return String(error);
2443
+ }
2444
+ function resolveTransportType(config) {
2445
+ return config.transport ?? "stdio";
2446
+ }
2447
+ function resolveStdioConfig(config) {
2448
+ const stdioConfig = config;
2449
+ return {
2450
+ command: requireNonEmptyString(stdioConfig.command, "command", config.name),
2451
+ args: stdioConfig.args,
2452
+ env: stdioConfig.env,
2453
+ cwd: stdioConfig.cwd
2454
+ };
2455
+ }
2456
+ function resolveRemoteHeaders(config) {
2457
+ const headers = {
2458
+ ...config.headers ?? {}
2459
+ };
2460
+ const accessToken = config.accessToken ?? resolveEnvToken(config.accessTokenEnvVar);
2461
+ if (accessToken && headers.Authorization === undefined) {
2462
+ headers.Authorization = `Bearer ${accessToken}`;
2463
+ }
2464
+ return Object.keys(headers).length > 0 ? headers : undefined;
2465
+ }
2466
+ function resolveEnvToken(envVarName) {
2467
+ if (!envVarName) {
2468
+ return;
2469
+ }
2470
+ const value = process.env[envVarName];
2471
+ if (!value) {
2472
+ return;
2473
+ }
2474
+ const trimmed = value.trim();
2475
+ return trimmed.length > 0 ? trimmed : undefined;
2476
+ }
2477
+ function requireNonEmptyString(value, field, serverName) {
2478
+ if (!value) {
2479
+ throw new Error(`MCP server "${serverName}" is missing required "${field}".`);
2480
+ }
2481
+ const trimmed = value.trim();
2482
+ if (trimmed.length === 0) {
2483
+ throw new Error(`MCP server "${serverName}" has an empty "${field}".`);
2484
+ }
2485
+ return trimmed;
2486
+ }
2487
+ var init_mcp_client_helpers = () => {};
2488
+
2489
+ // src/tools/mcp-client.ts
2490
+ import {
2491
+ experimental_createMCPClient
2492
+ } from "@ai-sdk/mcp";
2493
+ async function mcpServerToTools(config) {
2494
+ let client = null;
2495
+ try {
2496
+ const transport = buildMcpTransport(config);
2497
+ client = await experimental_createMCPClient({
2498
+ transport,
2499
+ name: config.clientName,
2500
+ version: config.clientVersion
2501
+ });
2502
+ const tools = await client.tools();
2503
+ const prefixedTools = prefixToolNames(config, tools);
2504
+ const connectedClient = client;
2505
+ return {
2506
+ tools: prefixedTools,
2507
+ cleanup: () => connectedClient.close(),
2508
+ serverToolNames: {
2509
+ [config.name]: Object.keys(prefixedTools)
2510
+ }
2511
+ };
2512
+ } catch (error) {
2513
+ if (client) {
2514
+ await client.close().catch(() => {
2515
+ return;
2516
+ });
2517
+ }
2518
+ throw new Error(`[MCP:${config.name}] Failed to connect tools: ${getErrorMessage(error)}`);
2519
+ }
2520
+ }
2521
+ async function createMcpToolsets(configs, options = {}) {
2522
+ const connected = [];
2523
+ try {
2524
+ for (const config of configs) {
2525
+ const result = await mcpServerToTools(config);
2526
+ connected.push(result);
2527
+ }
2528
+ } catch (error) {
2529
+ await Promise.allSettled(connected.map((result) => result.cleanup()));
2530
+ throw error;
2531
+ }
2532
+ const combinedTools = {};
2533
+ const serverToolNames = {};
2534
+ const collisionStrategy = options.onNameCollision ?? "overwrite";
2535
+ try {
2536
+ for (const result of connected) {
2537
+ for (const [serverName, toolNames] of Object.entries(result.serverToolNames)) {
2538
+ serverToolNames[serverName] = toolNames;
2539
+ }
2540
+ for (const [toolName, tool3] of Object.entries(result.tools)) {
2541
+ const hasCollision = combinedTools[toolName] !== undefined;
2542
+ if (hasCollision && collisionStrategy === "error") {
2543
+ throw new Error(`Duplicate MCP tool name "${toolName}" detected. Use "toolPrefix" or set onNameCollision to "overwrite".`);
2544
+ }
2545
+ combinedTools[toolName] = tool3;
2546
+ }
2547
+ }
2548
+ } catch (error) {
2549
+ await Promise.allSettled(connected.map((result) => result.cleanup()));
2550
+ throw error;
2551
+ }
2552
+ return {
2553
+ tools: combinedTools,
2554
+ serverToolNames,
2555
+ cleanup: async () => {
2556
+ const cleanupResults = await Promise.allSettled(connected.map((result) => result.cleanup()));
2557
+ const failures = cleanupResults.filter((result) => result.status === "rejected");
2558
+ if (failures.length > 0) {
2559
+ throw new Error(`Failed to cleanup ${failures.length} MCP client connection(s).`);
2560
+ }
2561
+ }
2562
+ };
2563
+ }
2564
+ var init_mcp_client = __esm(() => {
2565
+ init_mcp_client_helpers();
2566
+ });
2567
+
2403
2568
  // src/knowledge/injector.ts
2404
2569
  async function injectStaticKnowledge(instructions, knowledgeRefs, retriever, locale) {
2405
2570
  if (!retriever)
@@ -2909,25 +3074,59 @@ class ContractSpecAgent {
2909
3074
  tools;
2910
3075
  config;
2911
3076
  instructions;
3077
+ mcpCleanup;
2912
3078
  activeStepContexts = new Map;
2913
- constructor(config, instructions, tools) {
3079
+ constructor(config, instructions, tools, mcpCleanup) {
2914
3080
  this.config = config;
2915
3081
  this.spec = config.spec;
2916
3082
  this.id = agentKey(config.spec.meta);
2917
3083
  this.tools = tools;
2918
3084
  this.instructions = instructions;
3085
+ this.mcpCleanup = mcpCleanup;
2919
3086
  }
2920
3087
  static async create(config) {
2921
3088
  const effectiveConfig = config;
2922
- const instructions = await injectStaticKnowledge(effectiveConfig.spec.instructions, effectiveConfig.spec.knowledge ?? [], effectiveConfig.knowledgeRetriever);
2923
- const specTools = specToolsToAISDKTools(effectiveConfig.spec.tools, effectiveConfig.toolHandlers, { agentId: agentKey(effectiveConfig.spec.meta) });
2924
- const knowledgeTool = effectiveConfig.knowledgeRetriever ? createKnowledgeQueryTool(effectiveConfig.knowledgeRetriever, effectiveConfig.spec.knowledge ?? []) : null;
2925
- const tools = {
2926
- ...specTools,
2927
- ...knowledgeTool ? { query_knowledge: knowledgeTool } : {},
2928
- ...effectiveConfig.additionalTools ?? {}
2929
- };
2930
- return new ContractSpecAgent(effectiveConfig, instructions, tools);
3089
+ let mcpToolset = null;
3090
+ if ((effectiveConfig.mcpServers?.length ?? 0) > 0) {
3091
+ mcpToolset = await createMcpToolsets(effectiveConfig.mcpServers ?? [], {
3092
+ onNameCollision: "error"
3093
+ });
3094
+ }
3095
+ try {
3096
+ const instructions = await injectStaticKnowledge(effectiveConfig.spec.instructions, effectiveConfig.spec.knowledge ?? [], effectiveConfig.knowledgeRetriever);
3097
+ const specTools = specToolsToAISDKTools(effectiveConfig.spec.tools, effectiveConfig.toolHandlers, { agentId: agentKey(effectiveConfig.spec.meta) });
3098
+ const knowledgeTool = effectiveConfig.knowledgeRetriever ? createKnowledgeQueryTool(effectiveConfig.knowledgeRetriever, effectiveConfig.spec.knowledge ?? []) : null;
3099
+ const reservedToolNames = new Set(Object.keys(specTools));
3100
+ if (knowledgeTool) {
3101
+ reservedToolNames.add("query_knowledge");
3102
+ }
3103
+ const conflictingMcpTools = Object.keys(mcpToolset?.tools ?? {}).filter((toolName) => reservedToolNames.has(toolName));
3104
+ if (conflictingMcpTools.length > 0) {
3105
+ throw new Error(`MCP tools conflict with agent tools: ${conflictingMcpTools.join(", ")}. Configure MCP toolPrefix values to avoid collisions.`);
3106
+ }
3107
+ const tools = {
3108
+ ...specTools,
3109
+ ...knowledgeTool ? { query_knowledge: knowledgeTool } : {},
3110
+ ...mcpToolset?.tools ?? {},
3111
+ ...effectiveConfig.additionalTools ?? {}
3112
+ };
3113
+ return new ContractSpecAgent(effectiveConfig, instructions, tools, mcpToolset?.cleanup);
3114
+ } catch (error) {
3115
+ if (mcpToolset) {
3116
+ await mcpToolset.cleanup().catch(() => {
3117
+ return;
3118
+ });
3119
+ }
3120
+ throw error;
3121
+ }
3122
+ }
3123
+ async cleanup() {
3124
+ if (!this.mcpCleanup) {
3125
+ return;
3126
+ }
3127
+ const cleanup = this.mcpCleanup;
3128
+ this.mcpCleanup = undefined;
3129
+ await cleanup();
2931
3130
  }
2932
3131
  async generate(params) {
2933
3132
  const sessionId = params.options?.sessionId ?? generateSessionId();
@@ -3102,6 +3301,7 @@ var init_contract_spec_agent = __esm(() => {
3102
3301
  init_spec();
3103
3302
  init_tool_adapter();
3104
3303
  init_knowledge_tool();
3304
+ init_mcp_client();
3105
3305
  init_injector();
3106
3306
  init_adapter();
3107
3307
  ContractSpecCallOptionsSchema = z3.object({