Skip to content

refactor(mcpc): simplify API with AgentOptions and add defaults#46

Open
yaonyan wants to merge 1 commit intomainfrom
refactor/mcpc-api-v2
Open

refactor(mcpc): simplify API with AgentOptions and add defaults#46
yaonyan wants to merge 1 commit intomainfrom
refactor/mcpc-api-v2

Conversation

@yaonyan
Copy link
Contributor

@yaonyan 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

@yaonyan yaonyan requested a review from Copilot January 21, 2026 08:19
@yaonyan yaonyan self-assigned this 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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 single McpcConfig object containing server config, agents, plugins, and setup callback
  • Adds AgentOptions interface to group advanced options (maxSteps, maxTokens, etc.)
  • Renames existing API to mcpcLegacy for 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) and agents (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

Comment on lines +266 to +268
mode: agent.mode,
// Spread advanced options from agent.options
...agent.options,
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
mode: agent.mode,
// Spread advanced options from agent.options
...agent.options,
// Spread advanced options from agent.options
...agent.options,
mode: agent.mode,

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +74
transportType: "memory" as any,
server: testServer,
} as any,
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Adding official support for in-memory transport to McpServerDef (add a server field and "memory" transport type), or
  2. 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.

Suggested change
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,
},

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +97
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)
});
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +100
// 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";
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +13
Deno.test("mcpc API - minimal config with defaults", async () => {
const server = await mcpc({
name: "test-server",
});

assertExists(server);
});
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment on lines +318 to +323
{
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,
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.`,
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
`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.`,

Copilot uses AI. Check for mistakes.
* 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.
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Update the documentation to match the examples (use plugins array), OR
  2. Update the examples to match the documentation (use setup callback)

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.

Suggested change
* 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.

Copilot uses AI. Check for mistakes.
Comment on lines +414 to +426
// 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);
}
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +165
/**
* 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");
});
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Registers the markdown-loader plugin in the plugins array
  2. Includes a markdown file path in the agents array
  3. Verifies the agent is successfully loaded and composed

Copilot uses AI. Check for mistakes.
@yaonyan yaonyan force-pushed the main branch 2 times, most recently from 099e6cb to f42a71d Compare January 23, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant