Skip to content

Commit 45b98d3

Browse files
committed
feat: basic tool checkin
1 parent 3b0e99a commit 45b98d3

7 files changed

Lines changed: 242 additions & 10 deletions

File tree

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,27 @@ mcp.serve();
2727
2828
### Resources
2929
30+
The handler registered for a given path will be called with the parameters from the path.
31+
32+
If you need to access underlying resources like files or databases, you can use the `resource` method.
33+
3034
```typescript
31-
mcp.resource("protocol://path-to-resource", params, async (context) => {
32-
return "Hello, world!";
35+
mcp.resource("file://:fileName/:id", async ({ fileName, id }, ctx) => {
36+
return {
37+
contents: [
38+
{
39+
uri: "whatever",
40+
type: "text",
41+
text: `File: ${fileName} with ID: ${id}`,
42+
},
43+
],
44+
};
3345
});
46+
```
47+
48+
### Tools
49+
50+
3451
3552
```
3653

lib/EasyMCP.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
22
import type {
33
MCPResourceRequestParams,
44
ResourceOptions,
5+
ResourceRequestFn,
56
ServerOptions,
7+
ToolOptions,
8+
ToolRequestFn,
69
} from "../types";
710
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
811
import ResourceManager, {
@@ -11,21 +14,26 @@ import ResourceManager, {
1114
} from "./ResourceManager";
1215
import {
1316
ListResourcesRequestSchema,
17+
ListToolsRequestSchema,
1418
ReadResourceRequestSchema,
19+
type ListToolsResult,
1520
type ReadResourceRequest,
1621
type ReadResourceResult,
1722
} from "@modelcontextprotocol/sdk/types.js";
23+
import ToolManager from "./ToolManager";
1824

1925
class EasyMCP {
2026
name: string;
2127
opts: ServerOptions;
2228
resourceManager: ResourceManager;
29+
toolManager: ToolManager;
2330
server: Server;
2431

2532
private constructor(name: string, opts: ServerOptions) {
2633
this.name = name;
2734
this.opts = opts;
2835
this.resourceManager = ResourceManager.create();
36+
this.toolManager = ToolManager.create();
2937
this.server = new Server(
3038
{
3139
name: this.name,
@@ -58,11 +66,11 @@ class EasyMCP {
5866
}
5967
}
6068

61-
resource(
62-
uri: string,
63-
fn: (params: MCPResourceRequestParams) => Promise<any>,
64-
opts: ResourceOptions = {},
65-
) {
69+
tool(fn: ToolRequestFn, name: string, opts: ToolOptions) {
70+
return this.toolManager.add(name, fn, opts);
71+
}
72+
73+
resource(uri: string, fn: ResourceRequestFn, opts: ResourceOptions = {}) {
6674
return this.resourceManager.add(
6775
{
6876
uri,
@@ -99,6 +107,14 @@ class EasyMCP {
99107
},
100108
);
101109
console.log("Registered ReadResource endpoint");
110+
111+
// Tools
112+
this.server.setRequestHandler(
113+
ListToolsRequestSchema,
114+
async (): Promise<ListToolsResult> => {
115+
return { tools: this.toolManager.list() };
116+
},
117+
);
102118
}
103119

104120
static create(name: string, opts: ServerOptions) {

lib/ToolManager.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { test, expect, beforeEach, describe } from "bun:test";
2+
import ToolManager, { ToolNotFoundError, ToolConverter } from "./ToolManager";
3+
import type { EasyMCPTool } from "../types";
4+
import { createMockTool } from "./mocks";
5+
6+
describe("ToolManager", () => {
7+
let toolManager: ToolManager;
8+
9+
beforeEach(() => {
10+
toolManager = ToolManager.create();
11+
});
12+
13+
test("create() should return a new instance of ToolManager", () => {
14+
expect(toolManager).toBeInstanceOf(ToolManager);
15+
});
16+
17+
test("add() should add a tool with a function", async () => {
18+
const mockTool = createMockTool();
19+
const mockFn = async () => ({ result: "Mock result" });
20+
toolManager.add(mockTool.name, mockFn, mockTool);
21+
const result = await toolManager.get(mockTool.name);
22+
expect(result).toEqual(expect.objectContaining(mockTool));
23+
});
24+
25+
test("get() should return the added tool", async () => {
26+
const mockTool1 = createMockTool();
27+
const mockTool2 = createMockTool();
28+
const mockFn1 = async () => ({ result: "Result 1" });
29+
const mockFn2 = async () => ({ result: "Result 2" });
30+
31+
toolManager.add(mockTool1.name, mockFn1, mockTool1);
32+
toolManager.add(mockTool2.name, mockFn2, mockTool2);
33+
34+
const result1 = await toolManager.get(mockTool1.name);
35+
const result2 = await toolManager.get(mockTool2.name);
36+
expect(result1).toEqual(expect.objectContaining(mockTool1));
37+
expect(result2).toEqual(expect.objectContaining(mockTool2));
38+
});
39+
40+
test("get() should throw ToolNotFoundError for non-existent tool", async () => {
41+
expect(toolManager.get("non-existent-tool")).rejects.toThrow(
42+
ToolNotFoundError,
43+
);
44+
});
45+
46+
test("list() should return all added tools", () => {
47+
const mockTools = [createMockTool(), createMockTool(), createMockTool()];
48+
49+
mockTools.forEach((tool) =>
50+
toolManager.add(tool.name, async () => ({}), tool),
51+
);
52+
53+
const listedTools = toolManager.list();
54+
expect(listedTools).toHaveLength(mockTools.length);
55+
expect(listedTools).toEqual(
56+
expect.arrayContaining(
57+
mockTools.map((tool) => expect.objectContaining(tool)),
58+
),
59+
);
60+
});
61+
62+
test("list() should return an empty array when no tools are added", () => {
63+
expect(toolManager.list()).toEqual([]);
64+
});
65+
});
66+
67+
describe("ToolConverter", () => {
68+
test("fromMCPTool should convert EasyMCPTool to Tool", () => {
69+
const mockTool = createMockTool();
70+
const easyMCPTool: EasyMCPTool = {
71+
...mockTool,
72+
fn: async () => ({}),
73+
};
74+
75+
const result = ToolConverter.fromMCPTool(easyMCPTool);
76+
expect(result).toEqual(mockTool);
77+
expect(result).not.toHaveProperty("fn");
78+
});
79+
80+
test("toMCPTool should convert Tool to EasyMCPTool", () => {
81+
const mockTool = createMockTool();
82+
const mockFn = async () => ({});
83+
84+
const result = ToolConverter.toMCPTool(mockTool, mockFn);
85+
expect(result).toEqual({
86+
...mockTool,
87+
fn: mockFn,
88+
});
89+
});
90+
});

lib/ToolManager.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2+
import type { EasyMCPTool, ToolOptions, ToolRequestFn } from "../types";
3+
4+
export class ToolError extends Error {}
5+
6+
export class ToolNotFoundError extends ToolError {
7+
message = "Tool not found";
8+
}
9+
10+
export class ToolConverter {
11+
static fromMCPTool(tool: EasyMCPTool): Tool {
12+
return {
13+
name: tool.name,
14+
description: tool.description,
15+
inputSchema: tool.inputSchema,
16+
};
17+
}
18+
19+
static toMCPTool(tool: Tool, fn: ToolRequestFn): EasyMCPTool {
20+
return {
21+
name: tool.name,
22+
description: tool.description,
23+
inputSchema: tool.inputSchema,
24+
fn,
25+
};
26+
}
27+
}
28+
29+
export default class ToolManager {
30+
private tools: Record<string, EasyMCPTool>;
31+
private constructor() {
32+
this.tools = {};
33+
}
34+
35+
add(name: string, fn: ToolRequestFn, opts: ToolOptions) {
36+
this.tools[name] = ToolConverter.toMCPTool(
37+
{
38+
name,
39+
inputSchema: opts.inputSchema ?? {},
40+
description: opts.description,
41+
},
42+
fn,
43+
);
44+
}
45+
46+
list() {
47+
return Object.values(this.tools).map(ToolConverter.fromMCPTool);
48+
}
49+
50+
async get(name: string) {
51+
const foundTool = this.tools[name];
52+
53+
if (!foundTool) {
54+
throw new ToolNotFoundError();
55+
}
56+
57+
return foundTool;
58+
}
59+
60+
static create() {
61+
return new ToolManager();
62+
}
63+
}

lib/mocks.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { faker } from "@faker-js/faker";
2+
import type { Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
3+
4+
export function createMockResource(): Resource {
5+
return {
6+
uri: faker.internet.url(),
7+
name: faker.lorem.word(),
8+
description: faker.lorem.sentence(),
9+
mimeType: faker.system.mimeType(),
10+
};
11+
}
12+
13+
export function createMockTool(): Tool {
14+
return {
15+
name: faker.lorem.word(),
16+
description: faker.lorem.sentence(),
17+
inputSchema: {
18+
type: "object",
19+
properties: {
20+
[faker.lorem.word()]: {
21+
type: faker.helpers.arrayElement(["string", "number", "boolean"]),
22+
},
23+
},
24+
required: [faker.lorem.word()],
25+
},
26+
};
27+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"module": "index.ts",
44
"type": "module",
55
"scripts": {
6-
"start": "bun run index.ts"
6+
"start": "bun run index.ts",
7+
"dev": "bun --watch index.ts"
78
},
89
"devDependencies": {
910
"@anatine/zod-mock": "^3.13.4",

types.d.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { Resource } from "@modelcontextprotocol/sdk/types.js";
1+
import type { Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
22

33
export type ServerOptions = {
44
version: `${number}.${number}.${number}`;
55
};
66

77
export type EasyMCPResource = Resource & {
8-
fn: Function;
8+
fn: ResourceRequestFn;
99
};
1010

1111
export type MCPResourceRequestParams = Record<string, string>;
@@ -15,3 +15,21 @@ export type ResourceOptions = Partial<{
1515
description: string;
1616
mimeType: string;
1717
}>;
18+
19+
export type ResourceRequestFn = (
20+
params: MCPResourceRequestParams,
21+
context: ResourceRequestContext,
22+
) => Promise<any>;
23+
24+
export type ResourceRequestContext = {
25+
uri: string;
26+
getResource: (uri: string) => Promise<any>;
27+
};
28+
29+
export type ToolRequestFn = (params: any) => Promise<any>;
30+
31+
export type ToolOptions = Pick<Tool, "inputSchema" | "description">;
32+
33+
export type EasyMCPTool = Tool & {
34+
fn: ToolRequestFn;
35+
};

0 commit comments

Comments
 (0)