refactor(mcpc): simplify API with AgentOptions and add defaults#46
refactor(mcpc): simplify API with AgentOptions and add defaults#46
Conversation
yaonyan
commented
Jan 21, 2026
- Add AgentOptions interface for advanced options (maxSteps, maxTokens, tracingEnabled, samplingConfig, providerOptions, acpSettings, refs)
- Simplify AgentDef from 13 fields to 6 fields (name, description, mcpServers, mode, plugins, options)
- Add global plugins support that apply to all agents
- Add default values: version='1.0.0', capabilities={ tools: { listChanged: true } }
- Add 'agent' singular field for simpler single-agent configs
- Add mcpc_api_test.ts with 10 tests covering new API
- Update examples to use new structure
- Add AgentOptions interface for advanced options (maxSteps, maxTokens, tracingEnabled, samplingConfig, providerOptions, acpSettings, refs)
- Simplify AgentDef from 13 fields to 6 fields (name, description, mcpServers, mode, plugins, options)
- Add global plugins support that apply to all agents
- Add default values: version='1.0.0', capabilities={ tools: { listChanged: true } }
- Add 'agent' singular field for simpler single-agent configs
- Add mcpc_api_test.ts with 10 tests covering new API
- Update examples to use new structure
6b34908 to
1b5bae5
Compare
There was a problem hiding this comment.
Pull request overview
This PR refactors the MCPC API from a positional arguments pattern to a single configuration object pattern, inspired by AI SDK's design. The goal is to simplify the API by consolidating server metadata, agent definitions, plugins, and setup callbacks into one configuration object, reducing AgentDef from 13 fields to 6 fields, and adding convenient defaults.
Changes:
- Introduces new
mcpc()API with singleMcpcConfigobject containing server config, agents, plugins, and setup callback - Adds
AgentOptionsinterface to group advanced options (maxSteps, maxTokens, etc.) - Renames existing API to
mcpcLegacyfor backward compatibility - Updates all examples and tests to use new API or explicitly use
mcpcLegacy - Adds default values: version='1.0.0', capabilities={ tools: { listChanged: true } }
- Supports both
agent(singular) andagents(array) fields for flexibility
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/mcpc.ts | New API implementation with interfaces and main mcpc() function |
| packages/core/mod.ts | Updates exports to include new API while keeping legacy exports |
| packages/core/tests/unit/mcpc_api_test.ts | New test suite with 10 tests covering the new API patterns |
| packages/core/examples/*.ts | All examples updated to use new API structure |
| packages/plugin-*/**.ts | Test and example files updated to use mcpcLegacy |
| packages/cli/src/app.ts | CLI updated to use mcpcLegacy |
| mode: agent.mode, | ||
| // Spread advanced options from agent.options | ||
| ...agent.options, |
There was a problem hiding this comment.
The options object is created by first setting mode and then spreading agent.options. If agent.options contains a mode field (which it shouldn't according to the AgentOptions interface), it would override the intended mode from agent.mode. While the types prevent this, the code structure is fragile.
Consider restructuring to make the intent clearer:
options: {
...agent.options,
mode: agent.mode,
}This way, agent.mode explicitly overrides any mode in options, making the precedence clear.
| mode: agent.mode, | |
| // Spread advanced options from agent.options | |
| ...agent.options, | |
| // Spread advanced options from agent.options | |
| ...agent.options, | |
| mode: agent.mode, |
| transportType: "memory" as any, | ||
| server: testServer, | ||
| } as any, |
There was a problem hiding this comment.
The workaround uses as any type casts to handle in-memory transport, which isn't supported in the McpServerDef interface. While this works, it bypasses type safety. Consider either:
- Adding official support for in-memory transport to McpServerDef (add a
serverfield and "memory" transport type), or - Documenting that in-memory transport requires using the legacy API or direct compose() calls
The current approach using as any is fragile and could break with future type changes.
| transportType: "memory" as any, | |
| server: testServer, | |
| } as any, | |
| // In-memory transport is not currently described in the McpServerDef type. | |
| // This example demonstrates a supported runtime configuration that the | |
| // type system does not yet model. | |
| // @ts-expect-error: "memory" is not part of the McpServerDef.transportType union. | |
| transportType: "memory", | |
| // @ts-expect-error: In-memory transport uses a direct server instance, which | |
| // McpServerDef does not yet support as a typed field. | |
| server: testServer, | |
| }, |
| Deno.test("mcpc API - global plugins applied to all agents", async () => { | ||
| const pluginCalls: string[] = []; | ||
|
|
||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| plugins: [ | ||
| { | ||
| name: "test-plugin", | ||
| beforeToolExecute: (ctx) => { | ||
| pluginCalls.push(`before:${ctx.toolName}`); | ||
| return undefined; | ||
| }, | ||
| }, | ||
| ], | ||
| agents: [ | ||
| { name: "agent-1", description: "First" }, | ||
| { name: "agent-2", description: "Second" }, | ||
| ], | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| // Plugin should be registered (actual execution tested elsewhere) | ||
| }); |
There was a problem hiding this comment.
The test for global plugins only verifies that the server is created, not that plugins are actually applied to agents. The comment says "Plugin should be registered (actual execution tested elsewhere)" but this test should at least verify the plugin was added to the server's plugin list.
Consider adding an assertion like:
const pluginNames = server.getPluginNames?.() ?? [];
assertEquals(pluginNames.includes("test-plugin"), true);| // Legacy API - Positional arguments pattern (deprecated) | ||
| export { | ||
| type ComposeDefinition, | ||
| type ComposeInput, | ||
| type ComposibleMCPConfig, | ||
| isMarkdownFile, | ||
| type MarkdownAgentLoader, | ||
| mcpc, | ||
| mcpc as mcpcLegacy, | ||
| type McpcOptions, | ||
| parseMcpcConfigs, | ||
| setMarkdownAgentLoader, | ||
| } from "./src/set-up-mcp-compose.ts"; | ||
|
|
||
| // Main API - Single configuration object pattern | ||
| export { | ||
| type AgentDef, | ||
| type AgentOptions, | ||
| mcpc, | ||
| type McpcConfig, | ||
| type McpServerDef, | ||
| registerMarkdownLoader, | ||
| type ServerCapabilities, | ||
| } from "./src/mcpc.ts"; |
There was a problem hiding this comment.
Both setMarkdownAgentLoader (legacy) and registerMarkdownLoader (new API) are exported, which could confuse users about which one to use. While registerMarkdownLoader internally calls setMarkdownAgentLoader to maintain compatibility, consider adding a deprecation notice to setMarkdownAgentLoader in the documentation to guide users toward the new API.
Alternatively, document that setMarkdownAgentLoader is for legacy code only and new code should use registerMarkdownLoader.
| Deno.test("mcpc API - minimal config with defaults", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| }); |
There was a problem hiding this comment.
The test for minimal config with defaults doesn't verify that default values are actually applied. The PR description states that default values include version='1.0.0' and capabilities={ tools: { listChanged: true } }, but there's no assertion checking these defaults.
Add assertions to verify the defaults:
assertEquals(server.serverInfo.name, "test-server");
assertEquals(server.serverInfo.version, "1.0.0");
// And verify capabilities if accessible| { | ||
| command: (def as any).command, | ||
| args: (def as any).args, | ||
| env: (def as any).env, | ||
| transportType: (def as any).transportType, | ||
| url: (def as any).url, |
There was a problem hiding this comment.
Multiple as any type casts are used when converting ComposeDefinition back to AgentDef. This suppresses type checking and could hide type mismatches. The ComposeDefinition.deps.mcpServers likely has a proper type that should be used instead of casting to any.
Consider defining the proper type for the MCP server configuration or importing the type from the MCPSetting definition to maintain type safety.
| if (!markdownAgentLoader) { | ||
| throw new Error( | ||
| `Cannot load Markdown agent file "${input}": Markdown loader not available. ` + | ||
| `Add "@mcpc/plugin-markdown-loader" to plugins, or use inline AgentDef objects.`, |
There was a problem hiding this comment.
The error message instructs users to "Add '@mcpc/plugin-markdown-loader' to plugins" but the documentation at lines 195-199 says loader plugins should use the setup callback instead. This creates confusion about the correct usage.
The error message should be updated to match the final decision on where loader plugins should be specified (either in plugins array or via setup callback).
| `Add "@mcpc/plugin-markdown-loader" to plugins, or use inline AgentDef objects.`, | |
| `Configure a Markdown loader via the mcpc() setup callback (setMarkdownAgentLoader), or use inline AgentDef objects.`, |
| * Global plugins applied to ALL agents. | ||
| * Use this for plugins that should affect every agent (e.g., logging, caching). | ||
| * | ||
| * For loader plugins (e.g., markdown-loader), use `setup` callback instead. |
There was a problem hiding this comment.
The documentation states "For loader plugins (e.g., markdown-loader), use setup callback instead" but examples 16 and 17 show markdown-loader being used in the plugins array, not in setup. This creates confusion about the correct usage pattern.
Either:
- Update the documentation to match the examples (use
pluginsarray), OR - Update the examples to match the documentation (use
setupcallback)
Given that the legacy API loads plugins from the options.plugins array before resolving inputs, using the plugins array seems more appropriate once the plugin loading order bug is fixed.
| * For loader plugins (e.g., markdown-loader), use `setup` callback instead. | |
| * Loader plugins (e.g., markdown-loader) should also be added here, so that | |
| * file-based agents (like "./agents/coding-agent.md") can be resolved before | |
| * composition. |
| // Compose each agent with global plugins merged | ||
| for (const agent of resolvedAgents) { | ||
| const composeDef = agentDefToComposeDefinition(agent, plugins); | ||
|
|
||
| // Load plugins for this agent (global + agent-specific) | ||
| if (composeDef.plugins) { | ||
| for (const plugin of composeDef.plugins) { | ||
| if (typeof plugin === "string") { | ||
| await server.loadPluginFromPath(plugin); | ||
| } else { | ||
| await server.addPlugin(plugin); | ||
| } | ||
| } |
There was a problem hiding this comment.
Global plugins are being merged with agent-specific plugins for every agent and then loaded multiple times. This means a global plugin specified once will be loaded N times (once per agent), which is inefficient and may cause duplicate plugin registration errors.
Global plugins should be loaded once before agent composition begins, not merged into each agent's plugin list. Only agent-specific plugins (from agent.plugins) should be loaded per-agent.
| /** | ||
| * Tests for the new mcpc API (single configuration object pattern) | ||
| */ | ||
| import { assertEquals, assertExists } from "@std/assert"; | ||
| import { type AgentDef, mcpc } from "../../mod.ts"; | ||
|
|
||
| Deno.test("mcpc API - minimal config with defaults", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - single agent with 'agent' field", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| agent: { | ||
| name: "test-agent", | ||
| description: "A test agent", | ||
| }, | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| const tools = server.getPublicTools(); | ||
| assertEquals(tools.length, 1); | ||
| assertEquals(tools[0].name, "test-agent"); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - multiple agents with 'agents' field", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| agents: [ | ||
| { name: "agent-1", description: "First agent" }, | ||
| { name: "agent-2", description: "Second agent" }, | ||
| ], | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| const tools = server.getPublicTools(); | ||
| assertEquals(tools.length, 2); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - agent + agents combined", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| agent: { name: "main-agent", description: "Main" }, | ||
| agents: [ | ||
| { name: "helper-1", description: "Helper 1" }, | ||
| { name: "helper-2", description: "Helper 2" }, | ||
| ], | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| const tools = server.getPublicTools(); | ||
| assertEquals(tools.length, 3); | ||
| // agent comes first | ||
| assertEquals(tools[0].name, "main-agent"); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - composition-only mode (name: null)", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| agent: { | ||
| name: null, | ||
| description: "Composition only, no tool created", | ||
| }, | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| const tools = server.getPublicTools(); | ||
| assertEquals(tools.length, 0); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - global plugins applied to all agents", async () => { | ||
| const pluginCalls: string[] = []; | ||
|
|
||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| plugins: [ | ||
| { | ||
| name: "test-plugin", | ||
| beforeToolExecute: (ctx) => { | ||
| pluginCalls.push(`before:${ctx.toolName}`); | ||
| return undefined; | ||
| }, | ||
| }, | ||
| ], | ||
| agents: [ | ||
| { name: "agent-1", description: "First" }, | ||
| { name: "agent-2", description: "Second" }, | ||
| ], | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| // Plugin should be registered (actual execution tested elsewhere) | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - agent with mode", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| agent: { | ||
| name: "agentic-tool", | ||
| description: "An agentic tool", | ||
| mode: "agentic", | ||
| }, | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - agent with advanced options", async () => { | ||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| agent: { | ||
| name: "sampling-agent", | ||
| description: "A sampling agent", | ||
| mode: "ai_sampling", | ||
| options: { | ||
| maxSteps: 30, | ||
| maxTokens: 64000, | ||
| tracingEnabled: true, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - setup callback", async () => { | ||
| let setupCalled = false; | ||
|
|
||
| const server = await mcpc({ | ||
| name: "test-server", | ||
| setup: (_s) => { | ||
| setupCalled = true; | ||
| // Can add custom tools here | ||
| }, | ||
| }); | ||
|
|
||
| assertExists(server); | ||
| assertEquals(setupCalled, true); | ||
| }); | ||
|
|
||
| Deno.test("mcpc API - AgentDef type export works", () => { | ||
| // Type check - should compile | ||
| const agent: AgentDef = { | ||
| name: "typed-agent", | ||
| description: "A typed agent", | ||
| mcpServers: { | ||
| "test-server": { | ||
| command: "echo", | ||
| args: ["hello"], | ||
| }, | ||
| }, | ||
| mode: "agentic", | ||
| plugins: [], | ||
| options: { | ||
| maxSteps: 10, | ||
| }, | ||
| }; | ||
|
|
||
| assertExists(agent); | ||
| assertEquals(agent.name, "typed-agent"); | ||
| }); |
There was a problem hiding this comment.
The test suite doesn't include tests for markdown file loading, which is a key feature demonstrated in examples 16 and 17. Given that there's a critical bug in how plugins are loaded (which breaks markdown file loading), tests should verify this functionality works correctly.
Add a test case that:
- Registers the markdown-loader plugin in the plugins array
- Includes a markdown file path in the agents array
- Verifies the agent is successfully loaded and composed
099e6cb to
f42a71d
Compare