Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 139 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

1,051 changes: 1,019 additions & 32 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"build": "shx rm -rf dist && bun build ./src/cli/index.ts --outdir ./dist --target node && shx cp -r src/templates dist/",
"docs:install": "bun install --cwd docs",
"docs:build": "bun run --cwd docs build",
"docs:build": "bun run docs:install && bun run --cwd docs build",
"dev": "bun run src/cli/index.ts",
"test": "bun test",
"test:watch": "bun test --watch",
Expand Down Expand Up @@ -53,6 +53,7 @@
"homepage": "https://allagents.dev",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"chalk": "^5.6.2",
"cmd-ts": "^0.14.3",
"execa": "^8.0.1",
Expand Down
143 changes: 126 additions & 17 deletions src/cli/commands/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { dump } from 'js-yaml';
import {
array,
command,
Expand All @@ -9,19 +8,27 @@ import {
positional,
string,
} from 'cmd-ts';
import { dump } from 'js-yaml';
import {
addWorkspaceMcpServer,
buildMcpServerConfigFromFlags,
clearWorkspaceMcpServerProxy,
getWorkspaceMcpServer,
listWorkspaceMcpServers,
parseKeyValuePairs,
removeWorkspaceMcpServer,
setWorkspaceMcpServerProxy,
} from '../../core/mcp-servers.js';
import { runHttpMcpStdioProxy } from '../../core/mcp-http-stdio-proxy.js';
import { syncMcpOnly } from '../../core/mcp-sync.js';
import { ClientTypeSchema, type ClientType, type McpServerConfig } from '../../models/workspace-config.js';
import { isJsonMode, jsonOutput } from '../json-output.js';
import { buildDescription, conciseSubcommands } from '../help.js';
import {
type ClientType,
ClientTypeSchema,
type McpServerConfig,
} from '../../models/workspace-config.js';
import { formatMcpResult } from '../format-sync.js';
import { buildDescription, conciseSubcommands } from '../help.js';
import { isJsonMode, jsonOutput } from '../json-output.js';
import {
mcpAddMeta,
mcpGetMeta,
Expand Down Expand Up @@ -75,7 +82,10 @@ function buildConfigFromAddFlags(
client: string | undefined,
): McpServerConfig {
if (transport && transport !== 'http' && transport !== 'stdio') {
exitWithError(commandName, `Invalid transport '${transport}'. Expected 'http' or 'stdio'.`);
exitWithError(
commandName,
`Invalid transport '${transport}'. Expected 'http' or 'stdio'.`,
);
}

const envResult = parseKeyValuePairs(env, '-e/--env');
Expand Down Expand Up @@ -185,7 +195,8 @@ const addArgs = {
transport: option({
type: optional(string),
long: 'transport',
description: "Transport: 'http' or 'stdio' (auto-detected from URL if omitted)",
description:
"Transport: 'http' or 'stdio' (auto-detected from URL if omitted)",
}),
args: multioption({
type: array(string),
Expand All @@ -208,16 +219,35 @@ const addArgs = {
long: 'client',
description: 'Comma-separated list of client filters',
}),
proxy: flag({
long: 'proxy',
description:
'Rewrite HTTP MCP server sync through the built-in AllAgents HTTP proxy helper for the targeted clients',
}),
};

const mcpAddCmd = command({
name: 'add',
description: buildDescription(mcpAddMeta),
args: {
...addArgs,
force: flag({ long: 'force', short: 'f', description: 'Replace an existing server with the same name' }),
force: flag({
long: 'force',
short: 'f',
description: 'Replace an existing server with the same name',
}),
},
handler: async ({ name, commandOrUrl, transport, args, env, header, client, force }) => {
handler: async ({
name,
commandOrUrl,
transport,
args,
env,
header,
client,
proxy,
force,
}) => {
const config = buildConfigFromAddFlags(
'mcp add',
commandOrUrl,
Expand All @@ -227,12 +257,57 @@ const mcpAddCmd = command({
header,
client,
);
const addResult = await addWorkspaceMcpServer(name, config, process.cwd(), force);
if (!addResult.success) exitWithError('mcp add', addResult.error ?? 'Unknown error');
await runPostMutationSync('mcp add', `\u2713 Added MCP server '${name}' to workspace.yaml`, {
const proxyClients = client ? parseClientFilter(client) : undefined;
if (proxy && !('url' in config)) {
exitWithError(
'mcp add',
'--proxy is only supported for HTTP MCP servers',
);
}

const addResult = await addWorkspaceMcpServer(
name,
config: addResult.config,
});
config,
process.cwd(),
force,
);
if (!addResult.success)
exitWithError('mcp add', addResult.error ?? 'Unknown error');

if (proxy) {
const proxyResult = await setWorkspaceMcpServerProxy(
name,
process.cwd(),
proxyClients,
);
if (!proxyResult.success) {
exitWithError(
'mcp add',
proxyResult.error ?? 'Failed to persist MCP proxy config',
);
}
} else if (force) {
const clearResult = await clearWorkspaceMcpServerProxy(
name,
process.cwd(),
);
if (!clearResult.success) {
exitWithError(
'mcp add',
clearResult.error ?? 'Failed to clear MCP proxy config',
);
}
}

await runPostMutationSync(
'mcp add',
`\u2713 Added MCP server '${name}' to workspace.yaml`,
{
name,
config: addResult.config,
proxy,
},
);
},
});

Expand All @@ -248,7 +323,8 @@ const mcpRemoveCmd = command({
},
handler: async ({ name }) => {
const removeResult = await removeWorkspaceMcpServer(name, process.cwd());
if (!removeResult.success) exitWithError('mcp remove', removeResult.error ?? 'Unknown error');
if (!removeResult.success)
exitWithError('mcp remove', removeResult.error ?? 'Unknown error');
await runPostMutationSync(
'mcp remove',
`\u2713 Removed MCP server '${name}' from workspace.yaml`,
Expand All @@ -257,6 +333,30 @@ const mcpRemoveCmd = command({
},
});

// =============================================================================
// mcp proxy-stdio (internal)
// =============================================================================

const mcpProxyStdioCmd = command({
name: 'proxy-stdio',
description: 'Internal: expose a remote HTTP MCP server as local stdio',
args: {
serverUrl: positional({ type: string, displayName: 'serverUrl' }),
header: multioption({
type: array(string),
long: 'header',
description: 'HTTP header KEY=VALUE (repeatable)',
}),
},
handler: async ({ serverUrl, header }) => {
const headerResult = parseKeyValuePairs(header, '--header');
if ('error' in headerResult) {
exitWithError('mcp proxy-stdio', headerResult.error);
}
await runHttpMcpStdioProxy(serverUrl, headerResult.values);
},
});

// =============================================================================
// mcp list
// =============================================================================
Expand Down Expand Up @@ -322,7 +422,10 @@ const mcpGetCmd = command({
exitWithError('mcp get', e instanceof Error ? e.message : String(e));
}
if (!config) {
exitWithError('mcp get', `MCP server '${name}' not found in workspace.yaml`);
exitWithError(
'mcp get',
`MCP server '${name}' not found in workspace.yaml`,
);
}

if (isJsonMode()) {
Expand All @@ -346,7 +449,10 @@ const mcpUpdateCmd = command({
name: 'update',
description: buildDescription(mcpUpdateMeta),
args: {
offline: flag({ long: 'offline', description: 'Use cached plugins without fetching from remote' }),
offline: flag({
long: 'offline',
description: 'Use cached plugins without fetching from remote',
}),
},
handler: async ({ offline }) => {
const result = await syncMcpOnly(process.cwd(), { offline });
Expand All @@ -364,7 +470,9 @@ const mcpUpdateCmd = command({
}

const hasAnyChanges = Object.values(result.mcpResults).some(
(r) => r && (r.added > 0 || r.overwritten > 0 || r.removed > 0 || r.skipped > 0),
(r) =>
r &&
(r.added > 0 || r.overwritten > 0 || r.removed > 0 || r.skipped > 0),
);

if (!hasAnyChanges) {
Expand Down Expand Up @@ -398,6 +506,7 @@ export const mcpCmd = conciseSubcommands({
description: 'Manage MCP servers for AI clients',
cmds: {
add: mcpAddCmd,
'proxy-stdio': mcpProxyStdioCmd,
remove: mcpRemoveCmd,
list: mcpListCmd,
get: mcpGetCmd,
Expand Down
83 changes: 70 additions & 13 deletions src/cli/metadata/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ export const mcpAddMeta: AgentCommandMeta = {
'allagents mcp add my-server npx --arg=-y --arg=@my/mcp-server',
'allagents mcp add gh-api npx -e GH_TOKEN=abc123 --arg=-y --arg=@modelcontextprotocol/server-github',
'allagents mcp add deepwiki https://mcp.deepwiki.com/mcp --client claude,copilot',
'allagents mcp add secure-api https://api.example.com/mcp --proxy',
],
expectedOutput:
'Adds the server to workspace.yaml and syncs it to all configured clients. Exit 0 on success, 1 on failure.',
'Adds the server to workspace.yaml and syncs it to all configured clients. With --proxy, HTTP servers are rewritten through the built-in AllAgents HTTP-to-stdio proxy path for the targeted clients. Exit 0 on success, 1 on failure.',
positionals: [
{ name: 'name', type: 'string', required: true, description: 'Server name (unique within workspace.yaml)' },
{
name: 'name',
type: 'string',
required: true,
description: 'Server name (unique within workspace.yaml)',
},
{
name: 'commandOrUrl',
type: 'string',
Expand All @@ -24,12 +30,47 @@ export const mcpAddMeta: AgentCommandMeta = {
},
],
options: [
{ flag: '--transport', type: 'string', description: "Transport type: 'http' or 'stdio' (auto-detected from URL by default)" },
{ flag: '--arg', type: 'string', description: 'Argument to pass to the stdio command (repeatable)' },
{ flag: '--env', short: '-e', type: 'string', description: 'Environment variable KEY=VALUE for stdio transport (repeatable)' },
{ flag: '--header', type: 'string', description: 'HTTP header KEY=VALUE for http transport (repeatable)' },
{ flag: '--client', type: 'string', description: "Comma-separated list of clients that should receive this server (default: all project-scoped clients)" },
{ flag: '--force', short: '-f', type: 'boolean', description: 'Replace an existing server with the same name' },
{
flag: '--transport',
type: 'string',
description:
"Transport type: 'http' or 'stdio' (auto-detected from URL by default)",
},
{
flag: '--arg',
type: 'string',
description: 'Argument to pass to the stdio command (repeatable)',
},
{
flag: '--env',
short: '-e',
type: 'string',
description:
'Environment variable KEY=VALUE for stdio transport (repeatable)',
},
{
flag: '--header',
type: 'string',
description: 'HTTP header KEY=VALUE for http transport (repeatable)',
},
{
flag: '--client',
type: 'string',
description:
'Comma-separated list of clients that should receive this server (default: all project-scoped clients)',
},
{
flag: '--proxy',
type: 'boolean',
description:
'For HTTP servers, persist server-scoped proxy intent and sync targeted clients via the built-in AllAgents HTTP proxy helper',
},
{
flag: '--force',
short: '-f',
type: 'boolean',
description: 'Replace an existing server with the same name',
},
],
};

Expand All @@ -41,14 +82,20 @@ export const mcpRemoveMeta: AgentCommandMeta = {
expectedOutput:
'Removes the server from workspace.yaml and unsyncs it from all configured clients. Exit 0 on success, 1 if the server is not defined in workspace.yaml.',
positionals: [
{ name: 'name', type: 'string', required: true, description: 'Server name to remove' },
{
name: 'name',
type: 'string',
required: true,
description: 'Server name to remove',
},
],
};

export const mcpListMeta: AgentCommandMeta = {
command: 'mcp list',
description: 'List MCP servers defined in workspace.yaml',
whenToUse: 'To inspect MCP servers AllAgents is managing at the workspace level',
whenToUse:
'To inspect MCP servers AllAgents is managing at the workspace level',
examples: ['allagents mcp list'],
expectedOutput:
'Prints a table of workspace-defined MCP servers with transport, target, and client filter. Exit 0 on success.',
Expand All @@ -59,9 +106,15 @@ export const mcpGetMeta: AgentCommandMeta = {
description: 'Show the workspace definition for an MCP server',
whenToUse: 'To see how an MCP server is configured in workspace.yaml',
examples: ['allagents mcp get deepwiki'],
expectedOutput: 'Prints the server config (YAML). Exit 0 on success, 1 if not found.',
expectedOutput:
'Prints the server config (YAML). Exit 0 on success, 1 if not found.',
positionals: [
{ name: 'name', type: 'string', required: true, description: 'Server name' },
{
name: 'name',
type: 'string',
required: true,
description: 'Server name',
},
],
};

Expand All @@ -74,6 +127,10 @@ export const mcpUpdateMeta: AgentCommandMeta = {
expectedOutput:
'Runs the MCP portion of sync for all project-scoped clients. Prints per-scope added/updated/removed counts. Exit 0 on success, 1 on failure.',
options: [
{ flag: '--offline', type: 'boolean', description: 'Use cached plugins without fetching from remote' },
{
flag: '--offline',
type: 'boolean',
description: 'Use cached plugins without fetching from remote',
},
],
};
Loading