Skip to content

Commit 676fb44

Browse files
authored
Merge pull request #96 from QuantGeekDev/feat/tools-abstraction
Feat/tools abstraction
2 parents 8f34d26 + ef5eb5b commit 676fb44

19 files changed

+2422
-370
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ README
55
.DS_Store
66
dist
77
node_modules
8+
*.log
9+
coverage

README.md

Lines changed: 248 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,52 @@ mcp create <your project name here> --http --port 1337 --cors
7878
mcp add tool price-fetcher
7979
```
8080
81+
### Building and Validation
82+
83+
The framework provides comprehensive validation to ensure your tools are properly documented and functional:
84+
85+
```bash
86+
# Build with automatic validation (recommended)
87+
npm run build
88+
89+
# Build with custom validation settings
90+
MCP_SKIP_TOOL_VALIDATION=false npm run build # Force validation (default)
91+
MCP_SKIP_TOOL_VALIDATION=true npm run build # Skip validation (not recommended)
92+
```
93+
94+
### Validating Tools
95+
96+
```bash
97+
# Validate all tools have proper descriptions (for Zod schemas)
98+
mcp validate
99+
```
100+
101+
This command checks that all tools using Zod schemas have descriptions for every field. The validation runs automatically during build, but you can also run it standalone:
102+
103+
- ✅ **During build**: `npm run build` automatically validates tools
104+
- ✅ **Standalone**: `mcp validate` for manual validation
105+
- ✅ **Development**: Use `defineSchema()` helper for immediate feedback
106+
- ✅ **Runtime**: Server validates tools on startup
107+
108+
**Example validation error:**
109+
```bash
110+
❌ Tool validation failed:
111+
❌ PriceFetcher.js: Missing descriptions for fields in price_fetcher: symbol, currency.
112+
All fields must have descriptions when using Zod object schemas.
113+
Use .describe() on each field, e.g., z.string().describe("Field description")
114+
```
115+
116+
**Integrating validation into CI/CD:**
117+
```json
118+
{
119+
"scripts": {
120+
"build": "tsc && mcp-build",
121+
"test": "jest && mcp validate",
122+
"prepack": "npm run build && mcp validate"
123+
}
124+
}
125+
```
126+
81127
### Adding a Prompt
82128
83129
```bash
@@ -94,38 +140,75 @@ mcp add resource market-data
94140
95141
## Development Workflow
96142
97-
1. Create your project:
98-
99-
```bash
100-
mcp create my-mcp-server
101-
cd my-mcp-server
102-
```
103-
104-
2. Add tools as needed:
143+
1. **Create your project:**
144+
```bash
145+
mcp create my-mcp-server
146+
cd my-mcp-server
147+
```
105148
149+
2. **Add tools:**
106150
```bash
107151
mcp add tool data-fetcher
108152
mcp add tool data-processor
109153
mcp add tool report-generator
110154
```
111155
112-
3. Build:
156+
3. **Define your tool schemas with automatic validation:**
157+
```typescript
158+
// tools/DataFetcher.ts
159+
import { MCPTool, McpInput } from "mcp-framework";
160+
import { z } from "zod";
161+
162+
const DataFetcherSchema = z.object({
163+
// all fields should have .describe()
164+
url: z.string().url().describe("URL to fetch data from"),
165+
timeout: z.number().positive().default(5000).describe("Request timeout in milliseconds").optional()
166+
});
167+
168+
class DataFetcher extends MCPTool {
169+
name = "data_fetcher";
170+
description = "Fetch data from external APIs";
171+
schema = DataFetcherSchema;
172+
173+
async execute(input: McpInput<this>) {
174+
// Fully typed input with autocomplete support
175+
const { url, timeout = 5000 } = input;
176+
// ... implementation
177+
}
178+
}
179+
```
113180
181+
4. **Build with automatic validation:**
114182
```bash
115-
npm run build
183+
npm run build # Automatically validates schemas and compiles
184+
```
116185
186+
5. **Optional: Run standalone validation:**
187+
```bash
188+
mcp validate # Check all tools independently
117189
```
118190
119-
4. Add to MCP Client (Read below for Claude Desktop example)
191+
6. **Test your server:**
192+
```bash
193+
node dist/index.js # Server validates tools on startup
194+
```
195+
196+
7. **Add to MCP Client** (see Claude Desktop example below)
197+
198+
**Pro Tips:**
199+
- Use `defineSchema()` during development for immediate feedback
200+
- Build process automatically catches missing descriptions
201+
- Server startup validates all tools before accepting connections
202+
- Use TypeScript's autocomplete with `McpInput<this>` for better DX
120203
121204
## Using with Claude Desktop
122205
123206
### Local Development
124207
125208
Add this configuration to your Claude Desktop config file:
126209
127-
**MacOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
128-
**Windows**: \`%APPDATA%/Claude/claude_desktop_config.json\`
210+
**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
211+
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
129212
130213
```json
131214
{
@@ -142,8 +225,8 @@ Add this configuration to your Claude Desktop config file:
142225
143226
Add this configuration to your Claude Desktop config file:
144227
145-
**MacOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
146-
**Windows**: \`%APPDATA%/Claude/claude_desktop_config.json\`
228+
**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
229+
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
147230
148231
```json
149232
{
@@ -159,7 +242,7 @@ Add this configuration to your Claude Desktop config file:
159242
## Building and Testing
160243
161244
1. Make changes to your tools
162-
2. Run \`npm run build\` to compile
245+
2. Run `npm run build` to compile
163246
3. The server will automatically load your tools on startup
164247
165248
## Environment Variables
@@ -179,39 +262,170 @@ Example usage:
179262
MCP_ENABLE_FILE_LOGGING=true node dist/index.js
180263
181264
# Specify a custom log directory
182-
MCP_ENABLE_FILE_LOGGING=true MCP_LOG_DIRECTORY=my-logs
265+
MCP_ENABLE_FILE_LOGGING=true MCP_LOG_DIRECTORY=my-logs node dist/index.js
266+
183267
# Enable debug messages in console
184-
MCP_DEBUG_CONSOLE=true```
268+
MCP_DEBUG_CONSOLE=true node dist/index.js
269+
```
185270
186271
## Quick Start
187272
188-
### Creating a Tool
273+
### Defining Tools
274+
275+
MCP Framework uses Zod schemas for defining tool inputs, providing type safety, validation, and automatic documentation:
276+
277+
```typescript
278+
import { MCPTool, McpInput } from "mcp-framework";
279+
import { z } from "zod";
280+
281+
const AddToolSchema = z.object({
282+
a: z.number().describe("First number to add"),
283+
b: z.number().describe("Second number to add"),
284+
});
285+
286+
class AddTool extends MCPTool {
287+
name = "add";
288+
description = "Add tool description";
289+
schema = AddToolSchema;
290+
291+
async execute(input: McpInput<this>) {
292+
const result = input.a + input.b;
293+
return `Result: ${result}`;
294+
}
295+
}
296+
297+
export default AddTool;
298+
```
299+
300+
**Key Benefits:**
301+
- ✅ **Single source of truth** - Define types and validation in one place
302+
- ✅ **Automatic type inference** - TypeScript types are inferred from your schema
303+
- ✅ **Rich validation** - Leverage Zod's powerful validation features
304+
- ✅ **Required descriptions** - Framework enforces documentation
305+
- ✅ **Better IDE support** - Full autocomplete and type checking
306+
- ✅ **Cleaner code** - No duplicate type definitions
307+
308+
### Advanced Zod Schema Features
309+
310+
The framework supports all Zod features:
189311
190312
```typescript
191-
import { MCPTool } from "mcp-framework";
313+
import { MCPTool, McpInput } from "mcp-framework";
192314
import { z } from "zod";
193315

194-
interface ExampleInput {
195-
message: string;
316+
const AdvancedSchema = z.object({
317+
// String constraints and formats
318+
email: z.string().email().describe("User email address"),
319+
name: z.string().min(2).max(50).describe("User name"),
320+
website: z.string().url().optional().describe("Optional website URL"),
321+
322+
// Number constraints
323+
age: z.number().int().positive().max(120).describe("User age"),
324+
rating: z.number().min(1).max(5).describe("Rating from 1 to 5"),
325+
326+
// Arrays and objects
327+
tags: z.array(z.string()).describe("List of tags"),
328+
metadata: z.object({
329+
priority: z.enum(['low', 'medium', 'high']).describe("Task priority"),
330+
dueDate: z.string().optional().describe("Due date in ISO format")
331+
}).describe("Additional metadata"),
332+
333+
// Default values
334+
status: z.string().default('pending').describe("Current status"),
335+
336+
// Unions and enums
337+
category: z.union([
338+
z.literal('personal'),
339+
z.literal('work'),
340+
z.literal('other')
341+
]).describe("Category type")
342+
});
343+
344+
class AdvancedTool extends MCPTool {
345+
name = "advanced_tool";
346+
description = "Tool demonstrating advanced Zod features";
347+
schema = AdvancedSchema;
348+
349+
async execute(input: McpInput<this>) {
350+
// TypeScript automatically knows all the types!
351+
const { email, name, website, age, rating, tags, metadata, status, category } = input;
352+
353+
console.log(input.name.toUpperCase()); // ✅ TypeScript knows this is valid
354+
console.log(input.age.toFixed(2)); // ✅ Number methods available
355+
console.log(input.tags.length); // ✅ Array methods available
356+
console.log(input.website?.includes("https")); // ✅ Optional handling
357+
358+
return `Processed user: ${name}`;
359+
}
196360
}
361+
```
197362
198-
class ExampleTool extends MCPTool<ExampleInput> {
199-
name = "example_tool";
200-
description = "An example tool that processes messages";
363+
### Automatic Type Inference
201364
202-
schema = {
203-
message: {
204-
type: z.string(),
205-
description: "Message to process",
206-
},
207-
};
365+
The `McpInput<this>` type automatically infers the correct input type from your schema, eliminating the need for manual type definitions:
208366
209-
async execute(input: ExampleInput) {
210-
return `Processed: ${input.message}`;
367+
```typescript
368+
class MyTool extends MCPTool {
369+
schema = z.object({
370+
name: z.string().describe("User name"),
371+
age: z.number().optional().describe("User age"),
372+
tags: z.array(z.string()).describe("User tags")
373+
});
374+
375+
async execute(input: McpInput<this>) {
376+
// TypeScript automatically knows:
377+
// input.name is string
378+
// input.age is number | undefined
379+
// input.tags is string[]
380+
381+
console.log(input.name.toUpperCase()); // ✅ TypeScript knows this is valid
382+
console.log(input.age?.toFixed(2)); // ✅ Handles optional correctly
383+
console.log(input.tags.length); // ✅ Array methods available
211384
}
212385
}
386+
```
387+
388+
No more duplicate interfaces or generic type parameters needed!
389+
390+
### Schema Validation & Descriptions
391+
392+
**All schema fields must have descriptions**. This ensures your tools are well-documented and provides better user experience in MCP clients.
393+
394+
The framework validates descriptions at multiple levels:
395+
396+
#### 1. Build-time Validation (Recommended)
397+
```bash
398+
npm run build # Automatically validates during compilation
399+
```
400+
401+
#### 2. Development-time Validation
402+
Use the `defineSchema` helper for immediate feedback:
403+
404+
```typescript
405+
import { defineSchema } from "mcp-framework";
406+
407+
// This will throw an error immediately if descriptions are missing
408+
const MySchema = defineSchema({
409+
name: z.string(), // ❌ Error: Missing description
410+
age: z.number().describe("User age") // ✅ Good
411+
});
412+
```
413+
414+
#### 3. Standalone Validation
415+
```bash
416+
mcp validate # Check all tools for proper descriptions
417+
```
418+
419+
#### 4. Runtime Validation
420+
The server automatically validates tools on startup.
421+
422+
**To skip validation** (not recommended):
423+
```bash
424+
# Skip during build
425+
MCP_SKIP_TOOL_VALIDATION=true npm run build
213426

214-
export default ExampleTool;
427+
# Skip during development
428+
NODE_ENV=production npm run dev
215429
```
216430
217431
### Setting up the Server

jest.config.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/** @type {import('jest').Config} */
2+
export default {
3+
preset: 'ts-jest/presets/default-esm',
4+
testEnvironment: 'node',
5+
extensionsToTreatAsEsm: ['.ts'],
6+
moduleNameMapper: {
7+
'^(\\.{1,2}/.*)\\.js$': '$1',
8+
},
9+
transform: {
10+
'^.+\\.tsx?$': [
11+
'ts-jest',
12+
{
13+
useESM: true,
14+
tsconfig: {
15+
module: 'Node16',
16+
moduleResolution: 'Node16',
17+
},
18+
},
19+
],
20+
},
21+
testMatch: ['**/tests/**/*.test.ts'],
22+
moduleFileExtensions: ['ts', 'js', 'json'],
23+
coverageDirectory: 'coverage',
24+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/cli/**', '!src/index.ts'],
25+
};

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
"lint": "eslint",
2626
"lint:fix": "eslint --fix",
2727
"format": "prettier --write \"src/**/*.ts\"",
28-
"prepare": "npm run build"
29-
"dev:pub": "rm -rf dist && npm run build && yalc publish --push"
28+
"prepare": "npm run build",
29+
"dev:pub": "rm -rf dist && npm run build && yalc publish --push",
30+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
31+
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
32+
"test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage"
3033
},
3134
"engines": {
3235
"node": ">=18.19.0"

0 commit comments

Comments
 (0)