Skip to content

Commit 855886d

Browse files
committed
feat: add template source code structure
- Add MCP server entry point with example tool registration - Create example tool implementation with input validation - Add validation utilities for common patterns (URL, date, timestamp) - Include comprehensive test coverage for all components
1 parent f6ab775 commit 855886d

File tree

6 files changed

+206
-0
lines changed

6 files changed

+206
-0
lines changed

src/index.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('MCP Server', () => {
4+
it('should export as ES module', async () => {
5+
// This test verifies the module can be imported
6+
const module = await import('./index.js');
7+
expect(module).toBeDefined();
8+
});
9+
});

src/index.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env node
2+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4+
import {
5+
CallToolRequestSchema,
6+
ListToolsRequestSchema,
7+
} from '@modelcontextprotocol/sdk/types.js';
8+
import { z } from 'zod';
9+
import { zodToJsonSchema } from 'zod-to-json-schema';
10+
import { exampleTool, ExampleToolSchema } from './tools/example.js';
11+
12+
const server = new Server(
13+
{
14+
name: 'mcp-server-template',
15+
version: '0.1.0',
16+
},
17+
{
18+
capabilities: {
19+
tools: {},
20+
},
21+
}
22+
);
23+
24+
// List available tools
25+
server.setRequestHandler(ListToolsRequestSchema, async () => {
26+
return {
27+
tools: [
28+
{
29+
name: 'example_tool',
30+
description: 'An example tool that echoes back the input',
31+
inputSchema: zodToJsonSchema(ExampleToolSchema),
32+
},
33+
],
34+
};
35+
});
36+
37+
// Handle tool calls
38+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
39+
const { name, arguments: args } = request.params;
40+
41+
switch (name) {
42+
case 'example_tool':
43+
return await exampleTool(args);
44+
default:
45+
throw new Error(`Unknown tool: ${name}`);
46+
}
47+
});
48+
49+
// Start the server
50+
const transport = new StdioServerTransport();
51+
await server.connect(transport);
52+
53+
// Handle shutdown gracefully
54+
process.on('SIGINT', async () => {
55+
await server.close();
56+
process.exit(0);
57+
});

src/tools/example.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { exampleTool } from './example.js';
3+
4+
describe('exampleTool', () => {
5+
it('should echo the message', async () => {
6+
const result = await exampleTool({ message: 'Hello, world!' });
7+
expect(result.content[0]).toEqual({
8+
type: 'text',
9+
text: 'Echo: Hello, world!',
10+
});
11+
});
12+
13+
it('should convert to uppercase when requested', async () => {
14+
const result = await exampleTool({
15+
message: 'Hello, world!',
16+
uppercase: true,
17+
});
18+
expect(result.content[0]).toEqual({
19+
type: 'text',
20+
text: 'Echo: HELLO, WORLD!',
21+
});
22+
});
23+
24+
it('should validate input', async () => {
25+
await expect(exampleTool({})).rejects.toThrow();
26+
await expect(exampleTool({ message: 123 })).rejects.toThrow();
27+
});
28+
});

src/tools/example.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { z } from 'zod';
2+
3+
export const ExampleToolSchema = z.object({
4+
message: z.string().describe('The message to echo back'),
5+
uppercase: z.boolean().optional().default(false).describe('Whether to return the message in uppercase'),
6+
});
7+
8+
export type ExampleToolInput = z.infer<typeof ExampleToolSchema>;
9+
10+
export async function exampleTool(args: unknown) {
11+
const input = ExampleToolSchema.parse(args);
12+
13+
let result = input.message;
14+
if (input.uppercase) {
15+
result = result.toUpperCase();
16+
}
17+
18+
return {
19+
content: [
20+
{
21+
type: 'text',
22+
text: `Echo: ${result}`,
23+
},
24+
],
25+
};
26+
}

src/utils/validation.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { urlSchema, dateSchema, timestampSchema, validateInput } from './validation.js';
3+
4+
describe('validation schemas', () => {
5+
describe('urlSchema', () => {
6+
it('should accept valid URLs', () => {
7+
expect(urlSchema.parse('https://example.com')).toBe('https://example.com');
8+
expect(urlSchema.parse('http://example.com/path')).toBe('http://example.com/path');
9+
});
10+
11+
it('should reject invalid URLs', () => {
12+
expect(() => urlSchema.parse('not a url')).toThrow();
13+
expect(() => urlSchema.parse('example.com')).toThrow();
14+
});
15+
});
16+
17+
describe('dateSchema', () => {
18+
it('should accept valid dates', () => {
19+
expect(dateSchema.parse('2024-01-01')).toBe('2024-01-01');
20+
expect(dateSchema.parse('2024-12-31')).toBe('2024-12-31');
21+
});
22+
23+
it('should reject invalid dates', () => {
24+
expect(() => dateSchema.parse('2024-1-1')).toThrow();
25+
expect(() => dateSchema.parse('01-01-2024')).toThrow();
26+
expect(() => dateSchema.parse('2024/01/01')).toThrow();
27+
});
28+
});
29+
30+
describe('timestampSchema', () => {
31+
it('should accept valid timestamps', () => {
32+
expect(timestampSchema.parse('20240101120000')).toBe('20240101120000');
33+
});
34+
35+
it('should reject invalid timestamps', () => {
36+
expect(() => timestampSchema.parse('2024-01-01')).toThrow();
37+
expect(() => timestampSchema.parse('202401011200')).toThrow();
38+
});
39+
});
40+
});
41+
42+
describe('validateInput', () => {
43+
it('should return parsed value for valid input', () => {
44+
const result = validateInput(urlSchema, 'https://example.com');
45+
expect(result).toBe('https://example.com');
46+
});
47+
48+
it('should throw formatted error for invalid input', () => {
49+
expect(() => validateInput(urlSchema, 'invalid')).toThrow('Validation failed');
50+
});
51+
});

src/utils/validation.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { z } from 'zod';
2+
3+
/**
4+
* Common validation schemas for reuse across tools
5+
*/
6+
7+
// URL validation
8+
export const urlSchema = z.string().url('Invalid URL format');
9+
10+
// Date validation
11+
export const dateSchema = z.string().regex(
12+
/^\d{4}-\d{2}-\d{2}$/,
13+
'Date must be in YYYY-MM-DD format'
14+
);
15+
16+
// Timestamp validation (YYYYMMDDHHmmss)
17+
export const timestampSchema = z.string().regex(
18+
/^\d{14}$/,
19+
'Timestamp must be in YYYYMMDDHHmmss format'
20+
);
21+
22+
/**
23+
* Validate and parse input with helpful error messages
24+
*/
25+
export function validateInput<T>(schema: z.ZodSchema<T>, input: unknown): T {
26+
try {
27+
return schema.parse(input);
28+
} catch (error) {
29+
if (error instanceof z.ZodError) {
30+
const issues = error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`);
31+
throw new Error(`Validation failed:\n${issues.join('\n')}`);
32+
}
33+
throw error;
34+
}
35+
}

0 commit comments

Comments
 (0)