Universal Command
Define commands once, deploy to CLI, API, and MCP automatically
Installation
npx universal-commandAsk AI about Universal Command
Powered by Claude Β· Grounded in docs
I know everything about Universal Command. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
@supernal/universal-command
Define your command once. Deploy it to CLI, API, and MCP automatically.
π¦ npm Β· π Docs Β· π Issues
The Problem
Building tools for the AI age means maintaining three separate implementations:
- CLI β Commander.js schema + handler + output formatting
- API β Next.js/Express schema + handler + auth + caching
- MCP β
@modelcontextprotocol/sdkschema + handler + capabilities
That's 3Γ the code, 3Γ the tests, and inevitable drift between interfaces.
Universal Command solves this: define your command once, generate all three.
Why Universal Command?
| Feature | Universal Command | Manual Implementation |
|---|---|---|
| Maintenance | Define once | Define 3Γ (CLI/API/MCP) |
| Drift risk | None (single source) | High (separate codebases) |
| Type safety | Full TypeScript | Varies by interface |
| Validation | Automatic | Manual per interface |
| Testing | Test handler once | Test 3Γ |
| Feature velocity | Add once, works everywhere | Add 3Γ |
Quick Example
import { UniversalCommand } from '@supernal/universal-command';
const userCreate = new UniversalCommand({
name: 'user create',
description: 'Create a new user',
input: {
parameters: [
{ name: 'name', type: 'string', required: true, description: 'User name' },
{ name: 'email', type: 'string', required: true, description: 'User email' },
{ name: 'role', type: 'string', default: 'user', enum: ['user', 'admin'] },
],
},
handler: async (args) => createUser(args),
});
// CLI β mycli user create --name Alice --email alice@example.com
const cli = userCreate.toCLI(); // Commander.Command
// API β POST /api/users/create
export const POST = userCreate.toNextAPI(); // Next.js route handler
// MCP β Claude / Cursor / Zed tool call
const tool = userCreate.toMCP(); // MCP tool definition
One definition. Three interfaces. Zero drift.
The MCP Moment
The Model Context Protocol is rapidly becoming the standard way AI agents invoke tools. Claude, Cursor, Zed, and a growing ecosystem already support it. Every developer tool needs an MCP interface β and that number is only going up.
π awesome-mcp-servers β Your tool is one definition away from being listed here.
With Universal Command, adding MCP support to an existing CLI takes one line. No separate schema. No duplicate handler. No drift.
Installation
npm install @supernal/universal-command
Install peer dependencies for the interfaces you need:
npm install commander # CLI
npm install next # Next.js API
npm install @modelcontextprotocol/sdk # MCP
Core Usage
Define your command
import { UniversalCommand } from '@supernal/universal-command';
export const issueCreate = new UniversalCommand({
name: 'issue create',
description: 'Create a GitHub issue',
input: {
parameters: [
{ name: 'title', type: 'string', required: true },
{ name: 'body', type: 'string' },
{ name: 'labels', type: 'array', items: { type: 'string' } },
],
},
output: {
type: 'json',
schema: {
type: 'object',
properties: { id: { type: 'number' }, url: { type: 'string' } },
},
},
handler: async (args) => {
return await octokit.issues.create({
owner: 'org',
repo: 'repo',
title: args.title,
body: args.body,
labels: args.labels,
});
},
});
Deploy to CLI
// cli.ts
import { Command } from 'commander';
import { issueCreate } from './commands/issue-create';
const program = new Command();
program.addCommand(issueCreate.toCLI());
program.parse();
$ mytool issue create --title "Bug: login fails" --labels bug,urgent
Deploy to Next.js API
// app/api/issues/create/route.ts
import { issueCreate } from '@/commands/issue-create';
export const POST = issueCreate.toNextAPI();
$ curl -X POST /api/issues/create \
-d '{"title":"Bug: login fails","labels":["bug","urgent"]}'
Deploy to MCP
// mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server';
import { issueCreate } from './commands/issue-create';
const server = new Server({ name: 'my-mcp-server' });
server.setRequestHandler('tools/list', async () => ({
tools: [issueCreate.toMCP()],
}));
server.setRequestHandler('tools/call', async (request) => {
if (request.params.name === 'issue_create') {
return await issueCreate.executeMCP(request.params.arguments);
}
});
Interface-Specific Overrides
Each interface can be customized without touching the shared handler:
new UniversalCommand({
name: 'data export',
// ...
cli: {
format: (data) => JSON.stringify(data, null, 2),
streaming: true,
},
api: {
method: 'GET',
cacheControl: { maxAge: 300, staleWhileRevalidate: 60 },
auth: { required: true, roles: ['admin'] },
},
mcp: {
resourceLinks: ['export://results'],
capabilities: ['streaming'],
},
});
Execution Context
The handler receives a context object with interface-specific info:
handler: async (args, context) => {
if (context.interface === 'cli') {
console.log('Running from terminal');
}
if (context.interface === 'api') {
const userId = context.request.headers.get('x-user-id');
}
return result;
};
Error Handling
Throw a CommandError β it's automatically formatted for each interface:
import { CommandError } from '@supernal/universal-command';
handler: async (args) => {
if (!isValid(args.email)) {
throw new CommandError('Invalid email', { code: 'INVALID_EMAIL', status: 400 });
}
return result;
};
- CLI: Human-readable message + non-zero exit code
- API:
{ error: ... }JSON with HTTP status - MCP: MCP error format
Runtime Server
Register multiple commands and serve them all at once:
import { createRuntimeServer } from '@supernal/universal-command';
const server = createRuntimeServer();
server.register(userCreate);
server.register(issueCreate);
Next.js (catch-all route)
// app/api/[...path]/route.ts
const server = createServer();
const handlers = server.getNextHandlers();
export const GET = handlers.GET;
export const POST = handlers.POST;
Express
app.use('/api', createServer().getExpressRouter());
MCP Server
await createServer().startMCP({
name: 'my-mcp-server',
version: '1.0.0',
transport: 'stdio',
});
Registry Pattern
For large projects, group commands in a registry:
// commands/index.ts
import { CommandRegistry } from '@supernal/universal-command';
export const registry = new CommandRegistry();
registry.register(userCreate);
registry.register(issueCreate);
// Generate all CLI commands
for (const cmd of registry.getAll()) {
program.addCommand(cmd.toCLI());
}
// Generate all Next.js routes (build-time)
import { generateNextRoutes } from '@supernal/universal-command/codegen';
await generateNextRoutes(registry, { outputDir: 'app/api', typescript: true });
// Outputs: app/api/users/create/route.ts, app/api/issues/create/route.ts, ...
Testing
Test the handler once β it works everywhere:
import { userCreate } from './commands/user-create';
test('creates user', async () => {
const result = await userCreate.execute(
{ name: 'Alice', email: 'alice@example.com' },
{ interface: 'test' }
);
expect(result.name).toBe('Alice');
});
Integration test helpers:
import { testCLI, testAPI, testMCP } from '@supernal/universal-command/testing';
test('CLI', async () => {
const output = await testCLI(userCreate, {
args: ['--name', 'Alice', '--email', 'alice@example.com'],
});
expect(output).toContain('Created user');
});
test('API', async () => {
const res = await testAPI(userCreate, {
method: 'POST',
body: { name: 'Alice', email: 'alice@example.com' },
});
expect(res.status).toBe(200);
});
test('MCP', async () => {
const result = await testMCP(userCreate, {
arguments: { name: 'Alice', email: 'alice@example.com' },
});
expect(result.content[0].text).toContain('Alice');
});
API Reference
UniversalCommand
class UniversalCommand<TInput, TOutput> {
constructor(schema: CommandSchema<TInput, TOutput>);
execute(args: TInput, context: ExecutionContext): Promise<TOutput>;
executeMCP(args: unknown): Promise<MCPToolResult>;
toCLI(): Command; // Commander.js
toNextAPI(): NextAPIRoute; // Next.js App Router
toExpressAPI(): ExpressRoute;
toMCP(): MCPToolDefinition;
validateArgs(args: unknown): ValidationResult<TInput>;
getAPIRoutePath(): string;
getMCPToolName(): string;
}
CommandSchema
interface CommandSchema<TInput, TOutput> {
name: string;
description: string;
category?: string;
input: {
parameters: Parameter[];
};
output?: {
type: 'json' | 'text' | 'stream';
schema?: JSONSchema;
};
handler: (args: TInput, context: ExecutionContext) => Promise<TOutput>;
cli?: CLIOptions;
api?: APIOptions;
mcp?: MCPOptions;
}
Parameter
interface Parameter {
name: string;
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
description?: string;
required?: boolean;
default?: any;
// Validation
enum?: any[];
min?: number;
max?: number;
pattern?: string;
items?: Parameter; // for array type
}
ExecutionContext
interface ExecutionContext {
interface: 'cli' | 'api' | 'mcp' | 'test';
projectRoot?: string;
request?: NextRequest | Request; // API
stdout?: NodeJS.WriteStream; // CLI
stderr?: NodeJS.WriteStream; // CLI
}
Roadmap
| Version | Status | Features |
|---|---|---|
| v1.0 | β Current | CLI, Next.js API, MCP, TypeScript |
| v1.1 | π Planned | Express.js, streaming, auto-generated docs |
| v2.0 | π Future | Hono, gRPC, GraphQL |
Contributing
Contributions welcome! See CONTRIBUTING.md.
This project uses Supernal Coding for requirement tracking. Pre-commit hooks run lint, type-check, and all 93 tests automatically.
License
MIT β See LICENSE
Built by Supernal Intelligence Β· Commander.js Β· Next.js Β· Model Context Protocol
