aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
1,173 lines (1,033 loc) • 38 kB
JavaScript
/**
* AIWG MCP CLI
*
* Command-line interface for AIWG MCP server operations.
*/
import { startServer, createServer } from './server.mjs';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import {
McpServerRegistry,
injectServers,
SUPPORTED_PROVIDERS,
getProviderConfigPath,
} from './registry.mjs';
import { McpProfileRegistry } from './profiles.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Print usage information
*/
function printUsage() {
console.log(`
AIWG MCP Server
Usage:
aiwg mcp serve [options] Start the MCP server
aiwg mcp install [target] Install AIWG MCP server into a provider
aiwg mcp info Show server capabilities
aiwg mcp add <name> [opts] Define an MCP server in the registry
aiwg mcp remove <name> Remove a server from the registry
aiwg mcp update <name> [opts] Update a server definition
aiwg mcp list List registered MCP servers
aiwg mcp inject [opts] Inject servers into provider configs
aiwg mcp profile <sub> Manage MCP profiles (named server subsets)
Server Options (for add/update):
--url <url> Server URL (for http/sse types)
--type <type> Server type: http (default), stdio, sse
--command <cmd> Command to run (for stdio type)
--args <a1,a2,...> Command arguments (comma-separated, for stdio)
--env <K=V,...> Environment variables (comma-separated K=V pairs)
--headers <K=V,...> HTTP headers (comma-separated K=V pairs)
--description <text> Optional description
Inject Options:
--provider <name> Target provider (claude-code, cursor, factory, codex, opencode, windsurf, warp)
--all Inject into all previously configured providers
--servers <a,b,...> Only inject specific servers (comma-separated names)
--dry-run Show what would change without writing
Serve Options:
--transport <type> Transport type: stdio (default), http
--port <number> Port for HTTP transport (default: 3100)
Examples:
# Define MCP servers
aiwg mcp add fortemi --url https://memory.s9.internal/mcp --type http
aiwg mcp add gitea --url https://mcp-gitea.integrolabs.net/mcp
aiwg mcp add mytools --type stdio --command npx --args mcp-server-mytools
# Inject into provider configs
aiwg mcp inject --provider claude-code
aiwg mcp inject --provider cursor --servers fortemi,gitea
aiwg mcp inject --all
# Update a server URL
aiwg mcp update fortemi --url https://new-url.internal/mcp
aiwg mcp inject --all # re-inject to all providers
# List and manage
aiwg mcp list
aiwg mcp remove fortemi
# Install AIWG's own MCP server
aiwg mcp install claude
aiwg mcp serve
aiwg mcp info
`);
}
/**
* Generate MCP client configuration
*/
async function generateConfig(target, projectDir = '.') {
const homeDir = process.env.HOME || process.env.USERPROFILE;
const configs = {
claude: {
path: path.join(projectDir, '.claude/settings.local.json'),
content: {
mcpServers: {
aiwg: {
command: 'aiwg',
args: ['mcp', 'serve'],
env: {
AIWG_ROOT: process.env.AIWG_ROOT || '~/.local/share/ai-writing-guide'
}
}
}
}
},
cursor: {
path: path.join(projectDir, '.cursor/mcp.json'),
content: {
mcpServers: {
aiwg: {
command: 'aiwg',
args: ['mcp', 'serve']
}
}
},
merge: (existing, content) => ({
...existing,
mcpServers: {
...(existing.mcpServers || {}),
...content.mcpServers
}
})
},
factory: {
// Factory stores MCP config at user level in ~/.factory/mcp.json
// or project level in .factory/mcp.json
path: projectDir === '.' || projectDir === 'global'
? path.join(homeDir, '.factory/mcp.json')
: path.join(projectDir, '.factory/mcp.json'),
content: {
mcpServers: {
aiwg: {
type: 'stdio',
command: 'aiwg',
args: ['mcp', 'serve'],
disabled: false
}
}
},
merge: (existing, content) => ({
...existing,
mcpServers: {
...(existing.mcpServers || {}),
...content.mcpServers
}
})
},
codex: {
// Codex stores config in ~/.codex/config.toml (TOML format)
path: path.join(homeDir, '.codex/config.toml'),
// We generate TOML snippet to append, not JSON
content: null,
toml: `
# AIWG MCP Server Configuration
# Add this section to your ~/.codex/config.toml
[mcp_servers.aiwg]
command = "aiwg"
args = ["mcp", "serve"]
startup_timeout_sec = 10.0
tool_timeout_sec = 60.0
enabled_tools = [
"workflow-run",
"artifact-read",
"artifact-write",
"template-render",
"agent-list"
]
`,
// Custom handler for TOML
handler: async (configPath, tomlContent) => {
// Check if config.toml exists
let existing = '';
try {
existing = await fs.readFile(configPath, 'utf-8');
} catch {
// File doesn't exist
}
// Check if AIWG MCP already configured
if (existing.includes('[mcp_servers.aiwg]')) {
console.log('AIWG MCP already configured in ~/.codex/config.toml');
return true;
}
// Append TOML config
const updated = existing.trimEnd() + '\n' + tomlContent.trim() + '\n';
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, updated);
console.log(`MCP configuration appended to: ${configPath}`);
console.log(`\nTo use AIWG MCP server with Codex:`);
console.log(` 1. Restart Codex CLI`);
console.log(` 2. AIWG tools will be available via MCP`);
return true;
}
},
openai: {
// Alias for codex
path: path.join(homeDir, '.codex/config.toml'),
alias: 'codex'
},
windsurf: {
// Windsurf stores MCP config at ~/.codeium/windsurf/mcp_config.json
path: path.join(homeDir, '.codeium/windsurf/mcp_config.json'),
content: {
mcpServers: {
aiwg: {
command: 'aiwg',
args: ['mcp', 'serve']
}
}
},
merge: (existing, content) => ({
...existing,
mcpServers: {
...(existing.mcpServers || {}),
...content.mcpServers
}
})
},
warp: {
// Warp configures MCP servers via UI only (Settings > AI > MCP Servers)
// There is no documented file-based config path
path: null,
content: null,
handler: async () => {
console.log(`Warp MCP Server Setup (UI-based)\n`);
console.log(`Warp configures MCP servers through its UI, not config files.`);
console.log(`\nTo add the AIWG MCP server to Warp:\n`);
console.log(` 1. Open Warp Terminal`);
console.log(` 2. Go to Settings > AI > MCP Servers`);
console.log(` 3. Click "Add MCP Server"`);
console.log(` 4. Configure:`);
console.log(` Name: aiwg`);
console.log(` Type: stdio`);
console.log(` Command: aiwg`);
console.log(` Args: mcp serve`);
console.log(` 5. Save and restart Warp\n`);
console.log(`Alternatively, use Warp's /add-mcp slash command.`);
return true;
}
},
vscode: {
// VS Code / Copilot stores MCP config in .vscode/mcp.json
path: path.join(projectDir === '.' ? process.cwd() : projectDir, '.vscode/mcp.json'),
content: {
servers: {
aiwg: {
type: 'stdio',
command: 'aiwg',
args: ['mcp', 'serve']
}
}
},
merge: (existing, content) => ({
...existing,
servers: {
...(existing.servers || {}),
...content.servers
}
})
},
copilot: {
// Alias for vscode
path: path.join(projectDir === '.' ? process.cwd() : projectDir, '.vscode/mcp.json'),
alias: 'vscode'
},
opencode: {
// OpenCode stores MCP config in opencode.json at project root or .opencode/
path: projectDir === '.' || projectDir === 'global'
? path.join(process.cwd(), 'opencode.json')
: path.join(projectDir, 'opencode.json'),
content: {
mcp: {
aiwg: {
type: 'local',
command: ['aiwg', 'mcp', 'serve']
}
}
},
merge: (existing, content) => ({
...existing,
mcp: {
...(existing.mcp || {}),
...content.mcp
}
}),
// Custom handler to handle both JSON and JSONC formats
handler: async (configPath, _, content, mergeFunc) => {
// Check multiple locations for opencode config
const locations = [
configPath,
path.join(path.dirname(configPath), '.opencode', 'opencode.jsonc'),
path.join(path.dirname(configPath), '.opencode', 'opencode.json')
];
let targetPath = configPath;
let existing = {};
// Find existing config
for (const loc of locations) {
try {
const rawContent = await fs.readFile(loc, 'utf-8');
// Strip JSONC comments for parsing
const jsonContent = rawContent
.replace(/\/\/.*$/gm, '')
.replace(/\/\*[\s\S]*?\*\//g, '');
existing = JSON.parse(jsonContent);
targetPath = loc;
break;
} catch {
// Continue to next location
}
}
// Check if AIWG MCP already configured
if (existing.mcp && existing.mcp.aiwg) {
console.log('AIWG MCP already configured in OpenCode config');
return true;
}
// Merge configuration
const merged = mergeFunc(existing, content);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, JSON.stringify(merged, null, 2));
console.log(`MCP configuration written to: ${targetPath}`);
console.log(`\nTo use AIWG MCP server with OpenCode:`);
console.log(` 1. Restart OpenCode`);
console.log(` 2. AIWG tools will be available via MCP`);
return true;
}
}
};
let config = configs[target];
if (!config) {
console.error(`Unknown target: ${target}`);
console.error(`Available targets: ${Object.keys(configs).join(', ')}`);
return false;
}
// Handle alias
if (config.alias) {
config = configs[config.alias];
}
// Handle custom handler (for TOML configs like Codex, or OpenCode JSON)
if (config.handler) {
return await config.handler(config.path, config.toml, config.content, config.merge);
}
// Ensure directory exists
await fs.mkdir(path.dirname(config.path), { recursive: true });
// Check if file exists and merge
let existing = {};
try {
const content = await fs.readFile(config.path, 'utf-8');
existing = JSON.parse(content);
} catch {
// File doesn't exist, start fresh
}
// Merge configuration using custom merge function if available
const merged = config.merge
? config.merge(existing, config.content)
: {
...existing,
...config.content,
mcpServers: {
...(existing.mcpServers || {}),
...config.content.mcpServers
}
};
await fs.writeFile(config.path, JSON.stringify(merged, null, 2));
console.log(`MCP configuration written to: ${config.path}`);
console.log(`\nTo use AIWG MCP server with ${target}:`);
console.log(` 1. Restart ${target}`);
console.log(` 2. AIWG tools and resources will be available`);
return true;
}
/**
* Show server capabilities
*/
async function showInfo() {
console.log(`
AIWG MCP Server v1.0.0
Protocol Version: 2025-11-25
TOOLS:
workflow-run Execute AIWG workflow (phase transitions, reviews)
artifact-read Read artifact from .aiwg/ directory
artifact-write Write artifact to .aiwg/ directory
template-render Render AIWG template with variables
agent-list List available AIWG agents
RESOURCES:
aiwg://prompts/catalog List of prompt templates
aiwg://templates/catalog List of document templates
aiwg://agents/catalog List of available agents
aiwg://prompts/{category}/{name} Specific prompt template
aiwg://templates/{fw}/{cat}/{name} Specific document template
aiwg://agents/{framework}/{name} Specific agent definition
PROMPTS:
decompose-task Break complex task into subtasks
parallel-execution Identify parallelizable work
recovery-protocol PAUSE→DIAGNOSE→ADAPT→RETRY→ESCALATE
TRANSPORTS:
stdio Standard input/output (default, for local use)
http Streamable HTTP (for remote/containerized use)
ENVIRONMENT:
AIWG_ROOT Path to AIWG installation (default: ~/.local/share/ai-writing-guide)
For more information:
https://github.com/jmagly/aiwg
`);
}
// ============================================
// Registry subcommand handlers
// ============================================
/**
* Parse --key value pairs from args
*/
function parseFlag(args, flag) {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
/**
* Parse comma-separated key=value pairs into an object
*/
function parseKVPairs(str) {
if (!str) return undefined;
const result = {};
for (const pair of str.split(',')) {
const eqIdx = pair.indexOf('=');
if (eqIdx === -1) continue;
result[pair.slice(0, eqIdx).trim()] = pair.slice(eqIdx + 1).trim();
}
return Object.keys(result).length > 0 ? result : undefined;
}
/**
* Handle `aiwg mcp add <name> [opts]`
*/
async function handleAdd(args) {
const positional = args.filter(a => !a.startsWith('--'));
const name = positional[0];
if (!name) {
console.error('Usage: aiwg mcp add <name> --url <url> [--type http|stdio|sse] [--command <cmd>] [--args <a,b>]');
process.exit(1);
}
const type = parseFlag(args, '--type') || 'http';
const url = parseFlag(args, '--url');
const command = parseFlag(args, '--command');
const argsStr = parseFlag(args, '--args');
const envStr = parseFlag(args, '--env');
const headersStr = parseFlag(args, '--headers');
const description = parseFlag(args, '--description');
if (type === 'stdio' && !command) {
console.error('Error: --command is required for stdio type servers');
process.exit(1);
}
if ((type === 'http' || type === 'sse') && !url) {
console.error(`Error: --url is required for ${type} type servers`);
process.exit(1);
}
const registry = new McpServerRegistry();
await registry.add({
name,
type,
url,
command,
args: argsStr ? argsStr.split(',') : undefined,
env: parseKVPairs(envStr),
headers: parseKVPairs(headersStr),
description,
});
console.log(`Added MCP server: ${name}`);
if (url) console.log(` URL: ${url}`);
if (command) console.log(` Command: ${command}`);
console.log(` Type: ${type}`);
console.log(`\nUse "aiwg mcp inject --provider <name>" to inject into a provider config.`);
}
/**
* Handle `aiwg mcp remove <name>`
*/
async function handleRemove(args) {
const name = args.filter(a => !a.startsWith('--'))[0];
if (!name) {
console.error('Usage: aiwg mcp remove <name>');
process.exit(1);
}
const registry = new McpServerRegistry();
await registry.remove(name);
console.log(`Removed MCP server: ${name}`);
console.log(`\nNote: This does not remove the server from provider configs.`);
console.log(`Re-run "aiwg mcp inject --all" to update provider configs.`);
}
/**
* Handle `aiwg mcp update <name> [opts]`
*/
async function handleUpdate(args) {
const positional = args.filter(a => !a.startsWith('--'));
const name = positional[0];
if (!name) {
console.error('Usage: aiwg mcp update <name> --url <url> [--type <type>] ...');
process.exit(1);
}
const updates = {};
const url = parseFlag(args, '--url');
const type = parseFlag(args, '--type');
const command = parseFlag(args, '--command');
const argsStr = parseFlag(args, '--args');
const envStr = parseFlag(args, '--env');
const headersStr = parseFlag(args, '--headers');
const description = parseFlag(args, '--description');
if (url !== undefined) updates.url = url;
if (type !== undefined) updates.type = type;
if (command !== undefined) updates.command = command;
if (argsStr !== undefined) updates.args = argsStr.split(',');
if (envStr !== undefined) updates.env = parseKVPairs(envStr);
if (headersStr !== undefined) updates.headers = parseKVPairs(headersStr);
if (description !== undefined) updates.description = description;
if (Object.keys(updates).length === 0) {
console.error('No updates provided. Use --url, --type, --command, etc.');
process.exit(1);
}
const registry = new McpServerRegistry();
await registry.update(name, updates);
console.log(`Updated MCP server: ${name}`);
for (const [key, value] of Object.entries(updates)) {
console.log(` ${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
}
console.log(`\nRe-run "aiwg mcp inject --all" to propagate changes to provider configs.`);
}
/**
* Handle `aiwg mcp list` (managed servers from registry)
*/
async function handleList() {
const registry = new McpServerRegistry();
const servers = await registry.list();
if (servers.length === 0) {
console.log('No MCP servers registered.');
console.log('\nUse "aiwg mcp add <name> --url <url>" to add one.');
return;
}
console.log(`MCP Servers (${servers.length}):\n`);
for (const server of servers) {
console.log(` ${server.name}`);
console.log(` Type: ${server.type}`);
if (server.url) console.log(` URL: ${server.url}`);
if (server.command) console.log(` Command: ${server.command}${server.args ? ' ' + server.args.join(' ') : ''}`);
if (server.description) console.log(` Description: ${server.description}`);
if (server.injectedProviders && server.injectedProviders.length > 0) {
console.log(` Injected into: ${server.injectedProviders.join(', ')}`);
}
console.log('');
}
console.log(`Registry: ${registry.getPath()}`);
}
/**
* Handle `aiwg mcp inject [opts]`
*
* Supports:
* --profile <name> resolve server set from profile registry (#890)
* --ephemeral write standalone config file, do NOT touch provider default (#890)
* --out <path> explicit output path for ephemeral mode
* --all inject into all previously configured providers (existing behavior)
* --provider <name> target provider
* --servers a,b explicit server filter
* --dry-run print what would change
*/
async function handleInject(args) {
const provider = parseFlag(args, '--provider');
const injectAll = args.includes('--all');
const serversStr = parseFlag(args, '--servers');
const dryRun = args.includes('--dry-run');
const projectDir = parseFlag(args, '--project') || '.';
const profileName = parseFlag(args, '--profile');
const ephemeral = args.includes('--ephemeral');
const outPath = parseFlag(args, '--out');
if (!provider && !injectAll) {
console.error('Usage: aiwg mcp inject --provider <name> [--profile <p>] [--ephemeral] [--servers a,b] [--dry-run]');
console.error(' aiwg mcp inject --all [--dry-run]');
console.error(`\nSupported providers: ${SUPPORTED_PROVIDERS.join(', ')}`);
process.exit(1);
}
const registry = new McpServerRegistry();
// Resolve server filter: --profile takes precedence over --servers
let serverFilter;
if (profileName) {
const profiles = new McpProfileRegistry();
const profile = await profiles.get(profileName);
if (!profile) {
const all = await profiles.list();
console.error(`Profile "${profileName}" not found.`);
if (all.length > 0) console.error(`Available profiles: ${all.map(p => p.name).join(', ')}`);
process.exit(1);
}
// Resolve to server names (expand __all__ later in injectServers)
serverFilter = profile.servers.includes('__all__')
? undefined // inject all
: profile.servers;
console.log(`Profile: ${profileName} (${profile.servers.length === 1 && profile.servers[0] === '__all__' ? 'all servers' : profile.servers.join(', ')})`);
} else if (serversStr) {
serverFilter = serversStr.split(',').map(s => s.trim());
}
let providers;
if (injectAll) {
providers = await registry.getInjectedProviders();
if (providers.length === 0) {
console.error('No providers have been injected before. Use --provider <name> first.');
process.exit(1);
}
} else {
const normalized = provider === 'claude' ? 'claude-code' : provider;
if (!SUPPORTED_PROVIDERS.includes(normalized) && normalized !== 'openai') {
console.error(`Unknown provider: ${provider}`);
console.error(`Supported providers: ${SUPPORTED_PROVIDERS.join(', ')}`);
process.exit(1);
}
providers = [normalized];
}
// Ephemeral mode — providers that don't support it
const EPHEMERAL_UNSUPPORTED = ['warp'];
const EPHEMERAL_SUPPORTED = ['claude-code', 'claude', 'codex', 'openai'];
if (ephemeral) {
for (const p of providers) {
if (EPHEMERAL_UNSUPPORTED.includes(p)) {
console.error(`Error: --ephemeral is not supported for provider "${p}".`);
console.error(` Warp configures MCP servers via its UI only. No file-based ephemeral config is available.`);
process.exit(1);
}
}
console.log(dryRun ? '[DRY RUN] Ephemeral mode — would write standalone config:' : 'Ephemeral mode — writing standalone config (default provider config NOT modified):');
} else if (dryRun) {
console.log('[DRY RUN] Would inject servers into:');
}
let totalInjected = 0;
for (const p of providers) {
if (ephemeral) {
// Generate a standalone ephemeral config file
const targetPath = outPath ?? path.join(
process.env.TMPDIR || '/tmp',
`aiwg-mcp-${profileName ?? 'custom'}-${p}-${Date.now()}.json`,
);
const allServers = await registry.list();
const servers = serverFilter
? allServers.filter(s => serverFilter.includes(s.name))
: allServers;
if (servers.length === 0) {
console.error(` ${p}: no servers to write`);
continue;
}
// Build ephemeral config in provider's format
const mcpKey = p === 'opencode' ? 'mcp' : 'mcpServers';
const mcpBlock = {};
for (const server of servers) {
if (p === 'codex' || p === 'openai') {
// TOML providers get a note — ephemeral TOML is handled by codex-runtime adapter
console.log(` ${p}: TOML ephemeral config requires the codex-runtime adapter.`);
console.log(` Use "aiwg session --provider codex --profile ${profileName}" instead.`);
continue;
}
const cfg = {};
if (server.type === 'stdio') {
cfg.command = server.command;
cfg.args = server.args || [];
if (server.env) cfg.env = server.env;
} else {
cfg.url = server.url;
if (server.headers) cfg.headers = server.headers;
}
mcpBlock[server.name] = cfg;
}
if (Object.keys(mcpBlock).length === 0) continue;
const config = { [mcpKey]: mcpBlock };
if (!dryRun) {
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
}
const prefix = dryRun ? '[DRY RUN] ' : '';
console.log(`${prefix}${p}: ${targetPath}`);
console.log(` ${prefix}Servers: ${Object.keys(mcpBlock).join(', ')}`);
if (!dryRun && (p === 'claude-code' || p === 'claude')) {
console.log(` Launch with: claude --mcp-config ${targetPath}`);
}
totalInjected += Object.keys(mcpBlock).length;
continue;
}
// Persistent injection (existing behavior)
const result = await injectServers(registry, p, {
servers: serverFilter,
projectDir,
dryRun,
});
if (result.error) {
console.error(` ${p}: ${result.error}`);
continue;
}
const prefix = dryRun ? '[DRY RUN] ' : '';
console.log(`${prefix}${p}: ${result.configPath}`);
if (result.serversInjected.length > 0) {
console.log(` ${prefix}Injected: ${result.serversInjected.join(', ')}`);
totalInjected += result.serversInjected.length;
}
if (result.alreadyPresent.length > 0) {
console.log(` ${prefix}Updated in place: ${result.alreadyPresent.join(', ')}`);
}
}
if (!dryRun && totalInjected > 0 && !ephemeral) {
console.log(`\nDone. Restart your provider(s) to pick up the changes.`);
}
}
// ============================================
// Profile subcommand handlers (#889)
// ============================================
/**
* Print profile subcommand usage
*/
function printProfileUsage() {
console.log(`
aiwg mcp profile — MCP server profiles (named server subsets)
Usage:
aiwg mcp profile add <name> --servers a,b,c [--description "..."]
aiwg mcp profile list
aiwg mcp profile show <name>
aiwg mcp profile edit <name> [--add-server x] [--remove-server y] [--description "..."]
aiwg mcp profile remove <name>
aiwg mcp profile import <file>
aiwg mcp profile export <name> [--out <file>]
aiwg mcp profile init-presets
Profiles let you define named subsets of your registered MCP servers:
aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
aiwg mcp profile show dev
aiwg mcp inject --provider claude --profile dev --ephemeral
aiwg session --provider claude --profile dev
Preset profiles (minimal, dev, ops, research, incident, full):
aiwg mcp profile init-presets
`);
}
/**
* Handle `aiwg mcp profile add <name> [opts]`
*/
async function handleProfileAdd(args) {
const positional = args.filter(a => !a.startsWith('--'));
const name = positional[0];
if (!name) {
console.error('Usage: aiwg mcp profile add <name> --servers a,b,c [--description "..."]');
process.exit(1);
}
const serversStr = parseFlag(args, '--servers');
const description = parseFlag(args, '--description');
if (!serversStr && name !== 'minimal') {
console.error('Warning: no --servers specified. Profile will start empty.');
}
const servers = serversStr ? serversStr.split(',').map(s => s.trim()).filter(Boolean) : [];
const profiles = new McpProfileRegistry();
const registry = new McpServerRegistry();
await profiles.add({ name, description, servers }, registry);
console.log(`Profile added: ${name}`);
if (description) console.log(` Description: ${description}`);
console.log(` Servers (${servers.length}): ${servers.length > 0 ? servers.join(', ') : '(none)'}`);
console.log(`\nUse "aiwg mcp inject --provider <p> --profile ${name}" to inject this profile.`);
}
/**
* Handle `aiwg mcp profile list`
*/
async function handleProfileList() {
const profiles = new McpProfileRegistry();
const all = await profiles.list();
if (all.length === 0) {
console.log('No profiles defined.');
console.log('\nCreate one: aiwg mcp profile add dev --servers git-gitea,memory-fortemi');
console.log('Or install presets: aiwg mcp profile init-presets');
return;
}
console.log(`MCP Profiles (${all.length}):\n`);
for (const p of all) {
const serverCount = p.servers.length === 1 && p.servers[0] === '__all__'
? 'all'
: String(p.servers.length);
console.log(` ${p.name.padEnd(16)} [${serverCount} server${serverCount === '1' ? '' : 's'}] ${p.description ?? ''}`);
}
console.log(`\nProfiles file: ${profiles.getPath()}`);
}
/**
* Handle `aiwg mcp profile show <name>`
*/
async function handleProfileShow(args) {
const name = args.filter(a => !a.startsWith('--'))[0];
if (!name) {
console.error('Usage: aiwg mcp profile show <name>');
process.exit(1);
}
const profiles = new McpProfileRegistry();
const profile = await profiles.get(name);
if (!profile) {
const all = await profiles.list();
console.error(`Profile "${name}" not found.`);
if (all.length > 0) {
console.error(`Available profiles: ${all.map(p => p.name).join(', ')}`);
}
process.exit(1);
}
console.log(`Profile: ${profile.name}`);
if (profile.description) console.log(`Description: ${profile.description}`);
console.log(`\nServers (${profile.servers.length}):`);
if (profile.servers.length === 0) {
console.log(' (none)');
} else if (profile.servers[0] === '__all__') {
console.log(' (all registered servers)');
} else {
// Resolve server configs
const registry = new McpServerRegistry();
for (const serverName of profile.servers) {
const server = await registry.get(serverName);
if (server) {
const detail = server.type === 'stdio'
? `stdio ${server.command}${server.args ? ' ' + server.args.join(' ') : ''}`
: `${server.type} ${server.url}`;
console.log(` ${serverName.padEnd(24)} ${detail}`);
if (server.description) console.log(` ${''.padEnd(24)} ${server.description}`);
} else {
console.log(` ${serverName.padEnd(24)} (not in registry — missing)`);
}
}
}
if (profile.providerOverrides && Object.keys(profile.providerOverrides).length > 0) {
console.log('\nProvider overrides:');
for (const [provider, overrides] of Object.entries(profile.providerOverrides)) {
console.log(` ${provider}:`);
if (overrides.toolDeny) console.log(` toolDeny: ${overrides.toolDeny.join(', ')}`);
if (overrides.toolAllow) console.log(` toolAllow: ${overrides.toolAllow.join(', ')}`);
}
}
if (profile.createdAt) console.log(`\nCreated: ${profile.createdAt}`);
if (profile.updatedAt) console.log(`Updated: ${profile.updatedAt}`);
}
/**
* Handle `aiwg mcp profile edit <name> [opts]`
*/
async function handleProfileEdit(args) {
const positional = args.filter(a => !a.startsWith('--'));
const name = positional[0];
if (!name) {
console.error('Usage: aiwg mcp profile edit <name> [--add-server x] [--remove-server y] [--description "..."]');
process.exit(1);
}
const addServer = parseFlag(args, '--add-server');
const removeServer = parseFlag(args, '--remove-server');
const description = parseFlag(args, '--description');
const changes = {
description,
addServers: addServer ? addServer.split(',').map(s => s.trim()) : undefined,
removeServers: removeServer ? removeServer.split(',').map(s => s.trim()) : undefined,
};
if (!description && !addServer && !removeServer) {
console.error('No changes specified. Use --add-server, --remove-server, or --description.');
process.exit(1);
}
const profiles = new McpProfileRegistry();
const registry = new McpServerRegistry();
const updated = await profiles.edit(name, changes, registry);
console.log(`Profile updated: ${name}`);
console.log(` Servers (${updated.servers.length}): ${updated.servers.join(', ') || '(none)'}`);
}
/**
* Handle `aiwg mcp profile remove <name>`
*/
async function handleProfileRemove(args) {
const name = args.filter(a => !a.startsWith('--'))[0];
if (!name) {
console.error('Usage: aiwg mcp profile remove <name>');
process.exit(1);
}
const profiles = new McpProfileRegistry();
await profiles.remove(name);
console.log(`Profile removed: ${name}`);
}
/**
* Handle `aiwg mcp profile import <file>`
*/
async function handleProfileImport(args) {
const filePath = args.filter(a => !a.startsWith('--'))[0];
if (!filePath) {
console.error('Usage: aiwg mcp profile import <file>');
process.exit(1);
}
const profiles = new McpProfileRegistry();
const result = await profiles.importFrom(filePath);
console.log(`Imported ${result.added} new profile(s), updated ${result.updated} existing.`);
}
/**
* Handle `aiwg mcp profile export <name> [--out <file>]`
*/
async function handleProfileExport(args) {
const positional = args.filter(a => !a.startsWith('--'));
const name = positional[0]; // optional — omit to export all
const outFile = parseFlag(args, '--out') || (name ? `${name}-profile.json` : 'mcp-profiles.json');
const profiles = new McpProfileRegistry();
await profiles.exportTo(outFile, name);
console.log(`Exported ${name ? `profile "${name}"` : 'all profiles'} to: ${outFile}`);
}
/**
* Handle `aiwg mcp profile init-presets`
*/
async function handleProfileInitPresets() {
const profiles = new McpProfileRegistry();
const result = await profiles.initPresets();
if (result.added === 0) {
console.log(`All ${result.total} preset profiles are already installed.`);
console.log('Use "aiwg mcp profile list" to view them.');
} else {
console.log(`Installed ${result.added} preset profile(s) (${result.total} total presets):`);
console.log(' minimal, dev, ops, research, incident, full');
console.log('\nNote: preset server names reference expected registry entries.');
console.log('Run "aiwg mcp list" to see which servers are registered.');
}
}
/**
* Route `aiwg mcp profile <subcommand>`
*/
async function handleProfile(args) {
const sub = args[0];
const subArgs = args.slice(1);
switch (sub) {
case 'add':
await handleProfileAdd(subArgs);
break;
case 'list':
case 'ls':
await handleProfileList();
break;
case 'show':
await handleProfileShow(subArgs);
break;
case 'edit':
await handleProfileEdit(subArgs);
break;
case 'remove':
case 'rm':
await handleProfileRemove(subArgs);
break;
case 'import':
await handleProfileImport(subArgs);
break;
case 'export':
await handleProfileExport(subArgs);
break;
case 'init-presets':
await handleProfileInitPresets();
break;
default:
if (sub) console.error(`Unknown profile subcommand: ${sub}\n`);
printProfileUsage();
process.exit(sub ? 1 : 0);
}
}
// ============================================
// Main CLI entry point
// ============================================
/**
* Main CLI entry point
*/
export async function main(args = process.argv.slice(2)) {
const command = args[0];
const subArgs = args.slice(1);
switch (command) {
case 'serve': {
// Parse options
const transportIdx = args.indexOf('--transport');
const transport = transportIdx !== -1 ? args[transportIdx + 1] : 'stdio';
if (transport === 'http') {
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 3100;
console.error(`HTTP transport not yet implemented. Use stdio for now.`);
console.error(`Would start on port ${port}`);
process.exit(1);
}
// --toolsets flag overrides AIWG_MCP_TOOLSETS env (#1332 / S18)
const toolsetsIdx = args.indexOf('--toolsets');
if (toolsetsIdx !== -1 && args[toolsetsIdx + 1]) {
process.env.AIWG_MCP_TOOLSETS = args[toolsetsIdx + 1];
}
await startServer();
break;
}
case 'install': {
// Parse install arguments (skip flags)
const installArgs = args.slice(1).filter(a => !a.startsWith('--'));
const target = installArgs[0] || 'claude';
const projectDir = installArgs[1] || '.';
// Check for --dry-run flag
if (args.includes('--dry-run')) {
const homeDir = process.env.HOME || process.env.USERPROFILE;
console.log(`[DRY RUN] Would generate MCP config for: ${target}`);
console.log(`[DRY RUN] Target directory: ${projectDir}`);
const configPaths = {
claude: '.claude/settings.local.json',
cursor: '.cursor/mcp.json',
factory: (projectDir === '.' || projectDir === 'global')
? path.join(homeDir, '.factory/mcp.json')
: path.join(projectDir, '.factory/mcp.json'),
codex: path.join(homeDir, '.codex/config.toml'),
openai: path.join(homeDir, '.codex/config.toml'),
vscode: '.vscode/mcp.json',
copilot: '.vscode/mcp.json',
opencode: (projectDir === '.' || projectDir === 'global')
? 'opencode.json'
: path.join(projectDir, 'opencode.json'),
windsurf: path.join(homeDir, '.codeium/windsurf/mcp_config.json'),
warp: '(UI-based — Settings > AI > MCP Servers)'
};
console.log(`[DRY RUN] Config file: ${configPaths[target] || 'unknown'}`);
break;
}
await generateConfig(target, projectDir);
break;
}
case 'info':
await showInfo();
break;
case 'add':
await handleAdd(subArgs);
break;
case 'remove':
case 'rm':
await handleRemove(subArgs);
break;
case 'update':
await handleUpdate(subArgs);
break;
case 'list':
case 'ls':
await handleList();
break;
case 'inject':
await handleInject(subArgs);
break;
case 'profile':
await handleProfile(subArgs);
break;
case '--help':
case '-h':
case 'help':
printUsage();
break;
default:
if (command) {
console.error(`Unknown command: ${command}`);
}
printUsage();
process.exit(command ? 1 : 0);
}
}
// Run if executed directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch(error => {
console.error('Error:', error.message);
process.exit(1);
});
}