@supernal/universal-command
v0.1.0
Published
Define commands once, deploy to CLI, API, and MCP automatically
Maintainers
Readme
@supernal/universal-command
Universal Command Abstraction for CLI, API, and MCP
Define your command once, deploy it everywhere.
The Problem
Building a command-line tool with API and AI integration requires maintaining three separate implementations:
// CLI (Commander.js)
program.command('requirement list')
.option('--status <status>')
.action(async (options) => { /* implementation */ });
// API (Next.js)
export async function GET(request: NextRequest) {
const status = request.nextUrl.searchParams.get('status');
// duplicate implementation
}
// MCP (Model Context Protocol)
{
name: 'requirement_list',
inputSchema: { /* duplicate schema */ },
handler: async (args) => { /* duplicate implementation */ }
}Result: 3x maintenance burden, drift risk, duplicated logic.
The Solution
Define once, deploy everywhere:
import { UniversalCommand } from '@supernal/universal-command';
export const requirementList = new UniversalCommand({
name: 'requirement list',
description: 'List all requirements',
input: {
parameters: [
{
name: 'status',
type: 'string',
description: 'Filter by status',
enum: ['draft', 'in-progress', 'done'],
},
],
},
// Single handler works everywhere
handler: async (args, context) => {
return await fetchRequirements({ status: args.status });
},
// Optional: CLI-specific formatting
cli: {
format: (results) => results.map((r) => `${r.id}: ${r.title}`).join('\n'),
},
});Deploy automatically:
// Generate CLI
const program = requirementList.toCLI();
// Generate Next.js API
export const GET = requirementList.toNextAPI();
// Generate MCP tool
const mcpTool = requirementList.toMCP();Features
🎯 Single Source of Truth
- Define command logic once
- CLI, API, MCP stay in sync automatically
- No duplication, no drift
🚀 Zero Overhead
- Thin wrappers around your handler
- No performance penalty
- Direct function calls
🔧 Framework Agnostic
- Bring your own handler implementation
- Works with any framework
- No vendor lock-in
📦 Interface-Specific Overrides
- CLI: Custom formatting, progress bars
- API: Caching, rate limiting, auth
- MCP: Resource links, capabilities
🛡️ Type Safe
- Full TypeScript support
- Input/output validation
- Compile-time error detection
🧪 Testable
- Test handler once, works everywhere
- Mock context for testing
- Integration test helpers
Installation
npm install @supernal/universal-commandPeer dependencies:
# For CLI generation
npm install commander
# For Next.js API generation
npm install next
# For MCP generation
npm install @modelcontextprotocol/sdkQuick Start
1. Define Your Command
// commands/user-create.ts
import { UniversalCommand } from '@supernal/universal-command';
export const userCreate = new UniversalCommand({
name: 'user create',
description: 'Create a new user',
category: 'users',
input: {
parameters: [
{
name: 'name',
type: 'string',
description: 'User name',
required: true,
},
{
name: 'email',
type: 'string',
description: 'User email',
required: true,
},
{
name: 'role',
type: 'string',
description: 'User role',
default: 'user',
enum: ['user', 'admin'],
},
],
},
output: {
type: 'json',
schema: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
},
handler: async (args, context) => {
// Your implementation
const user = await createUser({
name: args.name,
email: args.email,
role: args.role,
});
return user;
},
});2. Generate CLI
// cli.ts
import { Command } from 'commander';
import { userCreate } from './commands/user-create';
const program = new Command();
program.addCommand(userCreate.toCLI());
program.parse();$ mycli user create --name "Alice" --email "[email protected]"
Created user: [email protected] (ID: user-123)3. Generate Next.js API
// app/api/users/create/route.ts
import { userCreate } from '@/commands/user-create';
export const POST = userCreate.toNextAPI();$ curl -X POST https://api.example.com/users/create \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'
{"id":"user-123","name":"Alice","email":"[email protected]"}4. Generate MCP Tool
// mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server';
import { userCreate } from './commands/user-create';
const server = new Server({ name: 'my-mcp-server' });
server.setRequestHandler('tools/list', async () => ({
tools: [userCreate.toMCP()],
}));
server.setRequestHandler('tools/call', async (request) => {
if (request.params.name === 'user_create') {
return await userCreate.executeMCP(request.params.arguments);
}
});Advanced Usage
Interface-Specific Options
new UniversalCommand({
name: 'data export',
// ... base definition ...
// CLI-specific
cli: {
format: (data) => {
// Custom formatting for terminal
return JSON.stringify(data, null, 2);
},
streaming: true, // Support streaming output
progress: true, // Show progress bar
},
// API-specific
api: {
method: 'GET', // Default: infer from handler
cacheControl: {
maxAge: 300,
staleWhileRevalidate: 60,
},
rateLimit: {
requests: 100,
window: '1m',
},
auth: {
required: true,
roles: ['admin'],
},
},
// MCP-specific
mcp: {
resourceLinks: ['export://results'],
capabilities: ['streaming'],
},
});Execution Context
The context parameter provides interface-specific information:
handler: async (args, context) => {
switch (context.interface) {
case 'cli':
// CLI-specific logic
console.log('Running from CLI');
break;
case 'api':
// Access request object
const userId = context.request.headers.get('x-user-id');
break;
case 'mcp':
// MCP-specific logic
break;
}
return result;
};Validation
Input validation is automatic based on parameter definitions:
input: {
parameters: [
{
name: 'age',
type: 'number',
required: true,
min: 0,
max: 120,
},
{
name: 'email',
type: 'string',
required: true,
pattern: '^[^@]+@[^@]+\\.[^@]+$',
},
];
}Error Handling
handler: async (args, context) => {
if (!isValid(args.email)) {
throw new CommandError('Invalid email address', { code: 'INVALID_EMAIL', status: 400 });
}
return result;
};Errors are automatically formatted for each interface:
- CLI: Formatted error message with exit code
- API: JSON error response with status code
- MCP: MCP error format
Runtime Registration (No Code Generation)
For the simplest setup, use RuntimeServer to register commands and serve them directly:
import { createRuntimeServer } from '@supernal/universal-command';
const server = createRuntimeServer();
// Register commands
server.register(userCreate);
server.register(userList);
server.register(userDelete);
// Or define inline
server.command({
name: 'health check',
description: 'Check system health',
input: { parameters: [] },
output: { type: 'json' },
handler: async () => ({ status: 'ok' }),
});Serve as Next.js API
// app/api/[...path]/route.ts
import { createServer } from '@/lib/commands';
const server = createServer();
const handlers = server.getNextHandlers();
export const GET = handlers.GET;
export const POST = handlers.POST;
export const PUT = handlers.PUT;
export const DELETE = handlers.DELETE;Serve as Express API
import express from 'express';
import { createServer } from './commands';
const app = express();
app.use(express.json());
app.use('/api', createServer().getExpressRouter());
app.listen(3000);Serve as MCP Server
import { createServer } from './commands';
const server = createServer();
await server.startMCP({
name: 'my-mcp-server',
version: '1.0.0',
transport: 'stdio',
});List All Commands
for (const cmd of server.listCommands()) {
console.log(`${cmd.name} → API: ${cmd.apiPath}, MCP: ${cmd.mcpTool}`);
}Registry Pattern
For multiple commands, use a registry:
// commands/index.ts
import { CommandRegistry } from '@supernal/universal-command';
import { userCreate } from './user-create';
import { userList } from './user-list';
import { userDelete } from './user-delete';
export const registry = new CommandRegistry();
registry.register(userCreate);
registry.register(userList);
registry.register(userDelete);Generate CLI Program
import { Command } from 'commander';
import { registry } from './commands';
const program = new Command();
for (const cmd of registry.getAll()) {
program.addCommand(cmd.toCLI());
}
program.parse();Generate API Routes (Build-time)
// scripts/generate-routes.ts
import { registry } from '../commands';
import { generateNextRoutes } from '@supernal/universal-command/codegen';
await generateNextRoutes(registry, {
outputDir: 'app/api',
typescript: true,
});Generates:
app/api/
users/
create/
route.ts (auto-generated)
list/
route.ts (auto-generated)
delete/
route.ts (auto-generated)Generate MCP Server
import { Server } from '@modelcontextprotocol/sdk/server';
import { registry } from './commands';
import { createMCPServer } from '@supernal/universal-command/mcp';
const server = createMCPServer(registry, {
name: 'my-mcp-server',
version: '1.0.0',
});
server.connect(transport);Code Generation
CLI Generation (Runtime)
const cli = command.toCLI();
// Returns: Commander.CommandAPI Generation (Build-time or Runtime)
// Runtime (Next.js App Router)
export const GET = command.toNextAPI();
// Or build-time generation
import { generateNextRoutes } from '@supernal/universal-command/codegen';
await generateNextRoutes(registry, { outputDir: 'app/api' });MCP Generation (Runtime)
const mcpTool = command.toMCP();
// Returns: MCPToolDefinitionTesting
Test the Handler Once
import { userCreate } from './user-create';
test('creates user', async () => {
const result = await userCreate.execute(
{ name: 'Alice', email: '[email protected]' },
{ interface: 'test' } // Test context
);
expect(result.name).toBe('Alice');
});Integration Testing Helpers
import { testCLI, testAPI, testMCP } from '@supernal/universal-command/testing';
test('CLI integration', async () => {
const output = await testCLI(userCreate, {
args: ['--name', 'Alice', '--email', '[email protected]'],
});
expect(output).toContain('Created user');
});
test('API integration', async () => {
const response = await testAPI(userCreate, {
method: 'POST',
body: { name: 'Alice', email: '[email protected]' },
});
expect(response.status).toBe(200);
});
test('MCP integration', async () => {
const result = await testMCP(userCreate, {
arguments: { name: 'Alice', email: '[email protected]' },
});
expect(result.content[0].text).toContain('Alice');
});Architecture
┌─────────────────────────────────────────────────────┐
│ UniversalCommand Definition │
│ • name, description, category │
│ • input schema (parameters) │
│ • output schema │
│ • handler (core logic) │
│ • interface overrides (cli, api, mcp) │
└────────────────┬────────────────────────────────────┘
│
┌────────┼────────┐
│ │ │
┌───▼──┐ ┌──▼───┐ ┌──▼───┐
│ CLI │ │ API │ │ MCP │
│ Gen │ │ Gen │ │ Gen │
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
┌──▼──┐ ┌──▼──┐ ┌──▼──┐
│ CLI │ │ API │ │ MCP │
│ App │ │ App │ │ App │
└─────┘ └─────┘ └─────┘API Reference
UniversalCommand
class UniversalCommand<TInput, TOutput> {
constructor(schema: CommandSchema<TInput, TOutput>);
// Execute command
execute(args: TInput, context: ExecutionContext): Promise<TOutput>;
// Generate interfaces
toCLI(): Command;
toNextAPI(): NextAPIRoute;
toExpressAPI(): ExpressRoute;
toMCP(): MCPToolDefinition;
// Utilities
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>;
// Interface-specific options
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;
// API-specific
request?: NextRequest | Request;
// CLI-specific
stdout?: NodeJS.WriteStream;
stderr?: NodeJS.WriteStream;
}Comparison
| Feature | Universal Command | Manual Implementation | | -------------------- | -------------------- | ------------------------- | | Maintenance | Define once | Define 3x (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 once | Test 3x | | Documentation | Auto-generated | Manual | | Feature velocity | High (add once) | Low (add 3x) |
Real-World Examples
GitHub CLI + API + MCP
// Single definition
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' } },
],
},
handler: async (args) => {
return await octokit.issues.create({
owner: 'org',
repo: 'repo',
title: args.title,
body: args.body,
labels: args.labels,
});
},
});
// Deploy everywhere
const cli = issueCreate.toCLI(); // gh issue create
const api = issueCreate.toNextAPI(); // POST /api/issues
const mcp = issueCreate.toMCP(); // issue_create toolRoadmap
v1.0 (Current)
- ✅ Core abstraction
- ✅ CLI generation (Commander.js)
- ✅ Next.js API generation
- ✅ MCP generation
- ✅ TypeScript support
v1.1 (Planned)
- 🔄 Express.js API generation
- 🔄 Streaming support
- 🔄 Progress indicators
- 🔄 Auto-generated docs
v2.0 (Future)
- 📋 Hono API generation
- 📋 FastAPI generation (Python)
- 📋 gRPC generation
- 📋 GraphQL generation
Development Workflow
This project uses the Supernal Coding workflow system for requirement tracking and test traceability.
Requirements
All features are documented as requirements in .supernal/requirements/:
| Requirement | Description | Tests | | ------------------------------------------------------------------------- | ---------------------- | ----- | | REQ-UC-001 | Universal Command Core | 20 | | REQ-UC-002 | Command Registry | 16 | | REQ-UC-003 | Code Generators | 18 | | REQ-UC-004 | Scope Registry | 30 | | REQ-UC-005 | Testing Utilities | 9 |
Git Hooks
Pre-commit hooks ensure code quality:
- Lint-staged: Format and lint staged files
- Type check: TypeScript compilation
- Tests: All 93 tests must pass
# Hooks run automatically on commit
git commit -m "feat: add new feature"
# Run manually
npm run type-check && npm run test:ciPublishing to npm
For Maintainers
# Ensure you're logged in to npm
npm login
# Run tests and build
pnpm test:ci
pnpm build
# Publish (prepublishOnly will run automatically)
npm publish
# Or publish a beta version
npm publish --tag betaVersion Bumping
# Patch release (0.1.0 -> 0.1.1)
npm version patch
# Minor release (0.1.0 -> 0.2.0)
npm version minor
# Major release (0.1.0 -> 1.0.0)
npm version majorContributing
Contributions welcome! See CONTRIBUTING.md
License
MIT License - See LICENSE
Credits
Developed by Supernal Intelligence as part of the Supernal Coding project.
Inspired by the need to maintain CLI, API, and MCP interfaces for AI-assisted development tools.
Related Projects
- Commander.js - CLI framework
- Next.js - React framework with API routes
- Model Context Protocol - AI tool integration
- tRPC - End-to-end typesafe APIs
Support
- 📖 Documentation
- 💬 Discord
- 🐛 Issue Tracker
- ✉️ Email: [email protected]
