You've got a running MCP server. Now what?
The gap between "it works" and "it does something useful" is where most tutorials abandon you. This one won't.
We're going to add a real tool to a real MCP server — one that reads a JSON file, validates the shape, and returns structured data. The kind of thing you'd actually build.
Prerequisites
A working MCP server. If you don't have one yet:
npx @webbywisp/create-mcp-server my-server
cd my-server && npm install && npm run build
Done. Now let's add a tool.
What We're Building
A read_config tool that:
- Accepts a file path argument
- Reads a JSON file from disk
- Validates it's actually valid JSON
- Returns the parsed content + metadata
Simple but genuinely useful — the kind of tool you'd wire up to let Claude read your agent's config files.
Step 1: Create the Tool File
In a tool-server project, create src/tools/readConfig.ts:
import { z } from 'zod';
import { readFile } from 'fs/promises';
import { resolve, basename } from 'path';
export const readConfigSchema = {
name: 'read_config',
description: 'Read and parse a JSON config file, returning its contents and metadata',
inputSchema: {
type: 'object' as const,
properties: {
path: {
type: 'string',
description: 'Absolute or relative path to the JSON file',
},
validate_keys: {
type: 'array',
items: { type: 'string' },
description: 'Optional list of required top-level keys to validate',
},
},
required: ['path'],
},
};
const InputSchema = z.object({
path: z.string(),
validate_keys: z.array(z.string()).optional(),
});
export async function readConfig(args: unknown) {
const { path: filePath, validate_keys } = InputSchema.parse(args);
const absolutePath = resolve(filePath);
// Read the file
let raw: string;
try {
raw = await readFile(absolutePath, 'utf-8');
} catch (err: any) {
return {
content: [{
type: 'text' as const,
text: `Error reading file: ${err.message}`,
}],
isError: true,
};
}
// Parse JSON
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (err: any) {
return {
content: [{
type: 'text' as const,
text: `File is not valid JSON: ${err.message}`,
}],
isError: true,
};
}
// Validate required keys if specified
if (validate_keys && typeof parsed === 'object' && parsed !== null) {
const missing = validate_keys.filter(k => !(k in (parsed as Record<string, unknown>)));
if (missing.length > 0) {
return {
content: [{
type: 'text' as const,
text: `Config is missing required keys: ${missing.join(', ')}`
}],
isError: true,
};
}
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
file: basename(absolutePath),
path: absolutePath,
size_bytes: raw.length,
keys: typeof parsed === 'object' && parsed !== null ? Object.keys(parsed as object) : [],
content: parsed,
}, null, 2),
}],
};
}
Step 2: Register the Tool
Open src/index.ts. Find where tools are registered and add yours:
import { readConfigSchema, readConfig } from './tools/readConfig.js';
// In your tool listing handler:
case 'tools/list':
return {
tools: [
// ... existing tools
readConfigSchema,
],
};
// In your tool call handler:
case 'tools/call':
switch (request.params.name) {
// ... existing cases
case 'read_config':
return readConfig(request.params.arguments);
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
Step 3: Build and Test
npm run build
Test it manually with the MCP inspector or run the server and call it from Claude Desktop. If you built with create-mcp-server, you already have a README with the Claude Desktop config snippet.
Add your server to Claude Desktop's claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-server/dist/index.js"]
}
}
}
Restart Claude Desktop. Your tool appears automatically.
Step 4: Use the add Command (Faster Path)
If you're using @webbywisp/create-mcp-server v0.2.0+, there's a faster way:
# From inside your project directory
npx @webbywisp/create-mcp-server add tool read_config
This scaffolds the boilerplate for you. Still need to fill in the actual logic, but the wiring is done.
The Pattern
Every tool follows the same structure:
- Schema — name, description, inputSchema (JSON Schema format)
- Zod validator — parse and validate the incoming args
- Logic — do the actual thing
-
Return —
{ content: [{ type: 'text', text: '...' }] }or{ isError: true }
Once you've written two or three tools, it becomes muscle memory. The hard part is knowing what to build — not how to wire it.
Ideas for Your Next Tool
-
list_directory— return a tree of files with sizes and dates -
run_test— execute a specific test file, return pass/fail + output -
git_status— summarize repo state (branch, staged, unstaged changes) -
env_check— verify required environment variables are set -
fetch_api— hit an internal API endpoint and return the response
Each of these follows the exact same pattern above. Scaffold once, repeat forever.
Wrapping Up
Adding tools to MCP servers is genuinely straightforward once you have the pattern. The boilerplate is the annoying part — which is exactly why create-mcp-server exists.
Build the thing, not the setup.
npx @webbywisp/create-mcp-server my-server
Part of the webbywisp series on building AI tooling that does not suck.
Top comments (0)