Skip to content

Commit 333863e

Browse files
committed
feat: add experimental decorator sample file and support for roots
1 parent 80532a2 commit 333863e

File tree

5 files changed

+120
-55
lines changed

5 files changed

+120
-55
lines changed

README.md

+71-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# easy-mcp
22

3-
> EasyMCP is in beta, with a first release coming in January 2025. Please report any issues you encounter.
3+
> EasyMCP is usable but in beta. Please report any issues you encounter.
44
55
Easy MCP is the simplest way to create Model Context Protocol (MCP) servers in TypeScript.
66

7-
It hides the plumbing and definitions behind simple, easy-to-use functions, allowing you to focus on building your server. Easy MCP strives to mimic the syntax of popular server frameworks like Express, making it easy to get started.
7+
It hides the plumbing, formatting, and other boilerplate definitions behind simple decorators that wrap a function.
88

99
Easy MCP allows you to define the bare minimum of what you need to get started, or you can define more complex resources, templates, tools, and prompts.
1010

11+
## Features
12+
13+
- Define @Tools, @Prompts, @Resources, and @Roots with one call to a decorator. Every possible parameter that could be optional is optional and hidden unless you need it.
14+
- Automagically infers tool, prompt, and resource arguments. No input schema definition required!
15+
1116
## Installation
1217

1318
To install easy-mcp, run the following command in your project directory:
@@ -24,10 +29,71 @@ bun add easy-mcp
2429

2530
## Limitations
2631

27-
- No support for sampling
28-
- No support for SSE
32+
- No support for logging, yet
33+
- No support for sampling, yet
34+
- No support for SSE, yet
35+
36+
## Usage with Decorator API
37+
38+
This API is simpler and infers types and input configuration automatically. But it's experimental.
39+
40+
```typescript
41+
import EasyMCP from "./EasyMCP";
42+
import { Prompt } from "./decorators/Prompt";
43+
import { Resource } from "./decorators/Resource";
44+
import { Root } from "./decorators/Root";
45+
import { Tool } from "./decorators/Tool";
46+
47+
@Root({
48+
uri: "/my-sample-dir/photos",
49+
})
50+
@Root({
51+
uri: "/my-root-dir",
52+
name: "My laptop's root directory",
53+
})
54+
class ZachsMCP extends EasyMCP {
55+
@Tool({})
56+
addNum(name: string, age: number) {
57+
return `${name} of ${age} age`;
58+
}
59+
60+
@Tool({
61+
description: "A function with various parameter types",
62+
optionalParameters: ["active", "items", "age"],
63+
})
64+
exampleFunc(name: string, active?: string, items?: string[], age?: number) {
65+
return `exampleFunc called: name ${name}, active ${active}, items ${items}, age ${age}`;
66+
}
67+
68+
@Resource({
69+
uri: "hello-world",
70+
})
71+
helloWorld() {
72+
return "Hello, world!";
73+
}
74+
75+
@Resource({
76+
uriTemplate: "greeting/{name}",
77+
})
78+
greeting(name: string) {
79+
return `Hello, ${name}!`;
80+
}
81+
82+
@Prompt({
83+
name: "prompt test",
84+
description: "test",
85+
})
86+
myPrompt(name: string) {
87+
return `Prompting... ${name}`;
88+
}
89+
}
90+
91+
const mcp = new ZachsMCP({ version: "1.0.0" });
92+
console.log(mcp.name, "is now serving!");
93+
```
94+
2995

30-
## Usage
96+
## Usage with Express-like API
3197

3298
Here's a basic example of how to use easy-mcp:
3399

lib/SKETCH.ts example-experimental-decorators.ts

+13-17
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import EasyMCP from "./EasyMCP";
2-
import { Prompt } from "./decorators/Prompt";
3-
import { Resource } from "./decorators/Resource";
4-
import { Root } from "./decorators/Root";
5-
import { Tool } from "./decorators/Tool";
1+
import EasyMCP from "./lib/EasyMCP";
2+
import { Prompt } from "./lib/decorators/Prompt";
3+
import { Resource } from "./lib/decorators/Resource";
4+
import { Root } from "./lib/decorators/Root";
5+
import { Tool } from "./lib/decorators/Tool";
66

77
@Root({
8-
uri: "my sample root",
8+
uri: "/my-sample-dir/photos",
9+
})
10+
@Root({
11+
uri: "/my-root-dir",
12+
name: "My laptop's root directory",
913
})
1014
class ZachsMCP extends EasyMCP {
1115
@Tool({})
12-
addNum({ name, age }: { name: string; age: number }) {
13-
console.log(`${name} of ${age} age`);
16+
addNum(name: string, age: number) {
17+
return `${name} of ${age} age`;
1418
}
1519

1620
@Tool({
@@ -45,12 +49,4 @@ class ZachsMCP extends EasyMCP {
4549
}
4650

4751
const mcp = new ZachsMCP({ version: "1.0.0" });
48-
49-
mcp.root({
50-
uri: "/",
51-
resource: "hello",
52-
});
53-
54-
mcp.serve();
55-
56-
console.dir(mcp);
52+
console.log(mcp.name, "is now serving!");

lib/EasyMCP.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ export default class EasyMCP extends BaseMCP {
267267
super("", { version, description });
268268
this.name = this.constructor.name;
269269

270+
// Handle class-level Root decorators
271+
const rootConfigs = (this.constructor as any).rootConfigs;
272+
if (rootConfigs && Array.isArray(rootConfigs)) {
273+
rootConfigs.forEach((rootConfig) => {
274+
this.root(rootConfig);
275+
});
276+
}
277+
270278
const childMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
271279
.filter((method) => typeof this[method] === "function")
272280
.filter(
@@ -278,30 +286,36 @@ export default class EasyMCP extends BaseMCP {
278286

279287
childMethods.forEach((method) => {
280288
// Assuming the decorator has been run to wrap these functions, we should have one of these configs on the relevant method.
289+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
281290
if (this[method][metadataKey].toolConfig) {
291+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
282292
this.tool(this[method][metadataKey].toolConfig);
283293
}
284294

295+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
285296
if (this[method][metadataKey].promptConfig) {
297+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
286298
this.prompt(this[method][metadataKey].promptConfig);
287299
}
288-
300+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
289301
if (this[method][metadataKey].rootConfig) {
302+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
290303
this.root(this[method][metadataKey].rootConfig);
291304
}
292305

306+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
293307
if (this[method][metadataKey].resourceConfig) {
308+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
294309
this.resource(this[method][metadataKey].resourceConfig);
295310
}
296311

312+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
297313
if (this[method][metadataKey].resourceTemplateConfig) {
314+
// @ts-expect-error Due to decorator behavior we're doing some JS prototype hacking that triggers a TS error here
298315
this.template(this[method][metadataKey].resourceTemplateConfig);
299316
}
300317
});
301318

302-
console.log("EasyMCP created with tools:", this.toolManager.list());
303-
304-
// Start serving
305-
// this.serve();
319+
this.serve();
306320
}
307321
}

lib/decorators/Root.ts

+16-28
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
1-
import type { Root } from "@modelcontextprotocol/sdk/types.js";
2-
import { metadataKey } from "../MagicConfig";
3-
4-
export function Root(config: { uri: string }) {
5-
return function (
6-
target: any,
7-
propertyKey: string,
8-
descriptor: PropertyDescriptor,
9-
) {
10-
const originalMethod = descriptor.value;
11-
12-
const rootConfig: Root = {
13-
name: propertyKey,
14-
uri: config.uri,
15-
};
16-
17-
/**
18-
We add the Root configuration to the original method so that it lives on the functions prototype.
19-
20-
When we instantiate the class later, we have access to this config which we can then use to register the Root with the Root Manager.
21-
*/
22-
23-
if (!originalMethod[metadataKey]) {
24-
originalMethod[metadataKey] = {};
1+
/**
2+
* All other decorators wrap instance methods. This decorator wraps the class itself because Roots do not have logic or function params when they're fulfilled.
3+
*/
4+
export function Root(config: { uri: string; name?: string }) {
5+
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
6+
if (!constructor.hasOwnProperty("rootConfigs")) {
7+
Object.defineProperty(constructor, "rootConfigs", {
8+
value: [],
9+
writable: true,
10+
configurable: true,
11+
});
2512
}
2613

27-
if (rootConfig) {
28-
originalMethod[metadataKey].rootConfig = rootConfig;
29-
}
14+
(constructor as any).rootConfigs.push({
15+
uri: config.uri,
16+
name: config.name || constructor.name,
17+
});
3018

31-
return descriptor;
19+
return constructor;
3220
};
3321
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"type": "module",
55
"scripts": {
66
"start": "bun run index.ts",
7+
"start:decorators": "bun run example-experimental-decorators.ts",
78
"dev": "bun --watch index.ts"
89
},
910
"devDependencies": {

0 commit comments

Comments
 (0)