Skip to content

Commit 9565319

Browse files
committed
feat: add CLI functionality to complete dual-mode template
- Add CLI mode with commander, chalk, and ora dependencies - Implement src/cli.ts with example command supporting --uppercase option - Update src/index.ts to detect CLI vs MCP mode automatically - Add bin configuration for command-line usage - Create comprehensive CLI tests with 100% coverage - Both MCP server and CLI modes now fully functional - Template now provides complete "hello world" for both modes The template can now be used as: - MCP server: node dist/index.js (no args) - CLI tool: node dist/index.js example "message" [--uppercase] This addresses the gap where documentation promised CLI functionality but implementation was missing. Template now serves as complete foundation for dual-mode MCP projects.
1 parent 7a4ccfd commit 9565319

File tree

5 files changed

+343
-52
lines changed

5 files changed

+343
-52
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"version": "1.0.0",
44
"description": "Template for building MCP (Model Context Protocol) servers with TypeScript",
55
"main": "dist/index.js",
6+
"bin": {
7+
"mcp-template": "dist/index.js"
8+
},
69
"type": "module",
710
"scripts": {
811
"build": "tsc",
@@ -38,6 +41,9 @@
3841
],
3942
"dependencies": {
4043
"@modelcontextprotocol/sdk": "^1.12.1",
44+
"chalk": "^5.4.1",
45+
"commander": "^14.0.0",
46+
"ora": "^8.2.0",
4147
"zod": "^3.25.51",
4248
"zod-to-json-schema": "^3.24.1"
4349
},

src/cli.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { createCLI } from './cli.js';
3+
import * as exampleModule from './tools/example.js';
4+
5+
vi.mock('./tools/example.js');
6+
7+
describe('CLI', () => {
8+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
9+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
10+
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
14+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
15+
});
16+
17+
it('should create CLI program', () => {
18+
const program = createCLI();
19+
expect(program.name()).toBe('mcp-template');
20+
expect(program.description()).toContain('MCP template');
21+
});
22+
23+
it('should handle example command', async () => {
24+
vi.spyOn(exampleModule, 'exampleTool').mockResolvedValue({
25+
content: [
26+
{
27+
type: 'text',
28+
text: 'Echo: Hello World',
29+
},
30+
],
31+
});
32+
33+
const program = createCLI();
34+
await program.parseAsync(['node', 'cli', 'example', 'Hello World']);
35+
36+
expect(exampleModule.exampleTool).toHaveBeenCalledWith({
37+
message: 'Hello World',
38+
uppercase: false,
39+
});
40+
});
41+
42+
it('should handle example command with uppercase option', async () => {
43+
vi.spyOn(exampleModule, 'exampleTool').mockResolvedValue({
44+
content: [
45+
{
46+
type: 'text',
47+
text: 'Echo: HELLO WORLD',
48+
},
49+
],
50+
});
51+
52+
const program = createCLI();
53+
await program.parseAsync(['node', 'cli', 'example', 'Hello World', '--uppercase']);
54+
55+
expect(exampleModule.exampleTool).toHaveBeenCalledWith({
56+
message: 'Hello World',
57+
uppercase: true,
58+
});
59+
});
60+
61+
it('should handle errors gracefully', async () => {
62+
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(() => {
63+
throw new Error('Process exit called');
64+
});
65+
66+
vi.spyOn(exampleModule, 'exampleTool').mockRejectedValue(new Error('Test error'));
67+
68+
const program = createCLI();
69+
70+
try {
71+
await program.parseAsync(['node', 'cli', 'example', 'Hello World']);
72+
} catch (error) {
73+
// Expected to throw due to process.exit mock
74+
}
75+
76+
expect(mockProcessExit).toHaveBeenCalledWith(1);
77+
mockProcessExit.mockRestore();
78+
});
79+
});

src/cli.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @fileoverview Command-line interface for MCP template operations
3+
* @module cli
4+
*/
5+
6+
import chalk from 'chalk';
7+
import { Command } from 'commander';
8+
import ora from 'ora';
9+
import { exampleTool } from './tools/example.js';
10+
11+
/**
12+
* Create and configure the CLI command structure
13+
* @returns Configured Commander program instance
14+
* @description Sets up CLI commands for MCP template operations. This provides
15+
* command-line access to the same tools available via the MCP server interface.
16+
* @example
17+
* ```typescript
18+
* const program = createCLI();
19+
* await program.parseAsync(process.argv);
20+
* ```
21+
*/
22+
export function createCLI() {
23+
const program = new Command();
24+
25+
program
26+
.name('mcp-template')
27+
.description('CLI tool for MCP template operations')
28+
.version('1.0.0');
29+
30+
// Example tool command
31+
program
32+
.command('example <message>')
33+
.description('Run the example tool that echoes back the input')
34+
.option('-u, --uppercase', 'Convert the message to uppercase')
35+
.action(async (message: string, options: { uppercase?: boolean }) => {
36+
const spinner = ora('Running example tool...').start();
37+
try {
38+
const result = await exampleTool({
39+
message,
40+
uppercase: options.uppercase || false,
41+
});
42+
43+
spinner.succeed(chalk.green('Example tool completed!'));
44+
console.log(chalk.blue('Result:'), result.content[0].text);
45+
} catch (error) {
46+
spinner.fail(chalk.red('Error running example tool'));
47+
console.error(error);
48+
process.exit(1);
49+
}
50+
});
51+
52+
return program;
53+
}

src/index.ts

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,101 @@
11
#!/usr/bin/env node
22
/**
3-
* @fileoverview MCP server entry point that sets up and starts the server
3+
* @fileoverview Entry point that supports both MCP server and CLI modes
44
* @module index
55
*/
66

77
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
88
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
99
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
10-
import { z } from 'zod';
1110
import { zodToJsonSchema } from 'zod-to-json-schema';
11+
import { createCLI } from './cli.js';
1212
import { ExampleToolSchema, exampleTool } from './tools/example.js';
1313

1414
/**
15-
* Create the MCP server instance with configured capabilities
15+
* Determine if we're running in CLI mode
16+
* CLI mode is detected when command line arguments are provided beyond node and script name
1617
*/
17-
const server = new Server(
18-
{
19-
name: 'mcp-template',
20-
version: '0.1.0',
21-
},
22-
{
23-
capabilities: {
24-
tools: {},
25-
},
26-
},
27-
);
18+
const isCliMode = process.argv.length > 2;
2819

2920
/**
30-
* Register handler for listing available tools
31-
* @returns List of available tools with their schemas
21+
* Main entry point that handles both MCP and CLI modes
3222
*/
33-
server.setRequestHandler(ListToolsRequestSchema, async () => {
34-
return {
35-
tools: [
36-
{
37-
name: 'example_tool',
38-
description: 'An example tool that echoes back the input',
39-
inputSchema: zodToJsonSchema(ExampleToolSchema),
40-
},
41-
],
42-
};
43-
});
23+
async function main() {
24+
if (isCliMode) {
25+
// CLI Mode: Run as command-line tool
26+
const program = createCLI();
27+
await program.parseAsync(process.argv);
28+
} else {
29+
// MCP Mode: Run as MCP server
30+
await startMcpServer();
31+
}
32+
}
4433

4534
/**
46-
* Register handler for executing tool calls
47-
* @param request - The tool call request containing tool name and arguments
48-
* @returns Tool execution result
35+
* Start the MCP server with all configured tools and handlers
4936
*/
50-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
51-
const { name, arguments: args } = request.params;
37+
async function startMcpServer() {
38+
/**
39+
* Create the MCP server instance with configured capabilities
40+
*/
41+
const server = new Server(
42+
{
43+
name: 'mcp-template',
44+
version: '0.1.0',
45+
},
46+
{
47+
capabilities: {
48+
tools: {},
49+
},
50+
},
51+
);
5252

53-
switch (name) {
54-
case 'example_tool':
55-
return await exampleTool(args);
56-
default:
57-
throw new Error(`Unknown tool: ${name}`);
58-
}
59-
});
53+
/**
54+
* Register handler for listing available tools
55+
* @returns List of available tools with their schemas
56+
*/
57+
server.setRequestHandler(ListToolsRequestSchema, async () => {
58+
return {
59+
tools: [
60+
{
61+
name: 'example_tool',
62+
description: 'An example tool that echoes back the input',
63+
inputSchema: zodToJsonSchema(ExampleToolSchema),
64+
},
65+
],
66+
};
67+
});
6068

61-
/**
62-
* Start the MCP server using stdio transport
63-
*/
64-
const transport = new StdioServerTransport();
65-
await server.connect(transport);
69+
/**
70+
* Register handler for executing tool calls
71+
* @param request - The tool call request containing tool name and arguments
72+
* @returns Tool execution result
73+
*/
74+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
75+
const { name, arguments: args } = request.params;
6676

67-
/**
68-
* Handle graceful shutdown on SIGINT (Ctrl+C)
69-
*/
70-
process.on('SIGINT', async () => {
71-
await server.close();
72-
process.exit(0);
73-
});
77+
switch (name) {
78+
case 'example_tool':
79+
return await exampleTool(args);
80+
default:
81+
throw new Error(`Unknown tool: ${name}`);
82+
}
83+
});
84+
85+
/**
86+
* Start the MCP server using stdio transport
87+
*/
88+
const transport = new StdioServerTransport();
89+
await server.connect(transport);
90+
91+
/**
92+
* Handle graceful shutdown on SIGINT (Ctrl+C)
93+
*/
94+
process.on('SIGINT', async () => {
95+
await server.close();
96+
process.exit(0);
97+
});
98+
}
99+
100+
// Start the application
101+
main().catch(console.error);

0 commit comments

Comments
 (0)