kleanmcp 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +31 -0
- package/build.ts +30 -0
- package/bun.lock +64 -0
- package/example/README.md +31 -0
- package/example/bun.lock +75 -0
- package/example/index.ts +125 -0
- package/example/package.json +19 -0
- package/example/tsconfig.json +29 -0
- package/package.json +39 -0
- package/src/index.ts +131 -0
- package/src/types.ts +165 -0
- package/test/mcp.test.ts +65 -0
- package/test/testToolSet.ts +67 -0
- package/test/toolset.test.ts +42 -0
- package/tsconfig.dts.json +12 -0
- package/tsconfig.json +36 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export * from "elysia-jsonrpc/mcp";
|
|
2
|
+
export * from "elysia-jsonrpc/rpc";
|
|
3
|
+
export * from "elysia-jsonrpc";
|
|
4
|
+
|
|
5
|
+
export type ParamType =
|
|
6
|
+
| "string"
|
|
7
|
+
| "number"
|
|
8
|
+
| "boolean"
|
|
9
|
+
| "object"
|
|
10
|
+
| "Record<string, any>";
|
|
11
|
+
|
|
12
|
+
export type ParamDef = {
|
|
13
|
+
type: ParamType;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
default?: ParamDef["type"];
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ExtractType<T extends ParamDef["type"]> = T extends "string"
|
|
21
|
+
? string
|
|
22
|
+
: T extends "number"
|
|
23
|
+
? number
|
|
24
|
+
: T extends "boolean"
|
|
25
|
+
? boolean
|
|
26
|
+
: T extends "object"
|
|
27
|
+
? object
|
|
28
|
+
: T extends "Record<string, any>"
|
|
29
|
+
? Record<string, any>
|
|
30
|
+
: never;
|
|
31
|
+
|
|
32
|
+
export type MethodResult<
|
|
33
|
+
TResult,
|
|
34
|
+
TParams extends readonly ParamDef[],
|
|
35
|
+
TExtra = any,
|
|
36
|
+
> = {
|
|
37
|
+
name: string;
|
|
38
|
+
params: TParams;
|
|
39
|
+
callback: (...args: [...ParamsToTuple<TParams>, TExtra]) => Promise<TResult>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type NamedParam<N extends string, T extends ParamDef> = T & { name: N };
|
|
43
|
+
|
|
44
|
+
export type ParamsToTuple<T extends readonly ParamDef[]> = {
|
|
45
|
+
[K in keyof T]: T[K] extends ParamDef ? ExtractType<T[K]["type"]> : never;
|
|
46
|
+
};
|
|
47
|
+
export type NamedParamsToTuple<
|
|
48
|
+
T extends readonly NamedParam<string, ParamDef>[],
|
|
49
|
+
> = {
|
|
50
|
+
[K in keyof T]: T[K] extends NamedParam<string, infer P>
|
|
51
|
+
? P extends ParamDef
|
|
52
|
+
? ExtractType<P["type"]>
|
|
53
|
+
: never
|
|
54
|
+
: never;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export interface PropertyDescription {
|
|
58
|
+
type: string;
|
|
59
|
+
description: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface InputSchemaType {
|
|
63
|
+
[x: string]: unknown;
|
|
64
|
+
type: "object";
|
|
65
|
+
properties?: Record<string, PropertyDescription>;
|
|
66
|
+
required?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ToolParameterDefinition = {
|
|
70
|
+
required: boolean;
|
|
71
|
+
type: string;
|
|
72
|
+
description: string;
|
|
73
|
+
default?: object;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export interface CallToolResult {
|
|
77
|
+
content: any[];
|
|
78
|
+
isError?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function InputSchemaFromParameters(
|
|
82
|
+
params: ToolParameterDefinition[],
|
|
83
|
+
): InputSchemaType {
|
|
84
|
+
return {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: Object.fromEntries(
|
|
87
|
+
Object.entries(params).map(([k, v]) => [
|
|
88
|
+
k,
|
|
89
|
+
{
|
|
90
|
+
description: v.description,
|
|
91
|
+
type: v.type,
|
|
92
|
+
default: v.default,
|
|
93
|
+
} as PropertyDescription,
|
|
94
|
+
]),
|
|
95
|
+
) as Record<string, PropertyDescription>,
|
|
96
|
+
required: Object.entries(params)
|
|
97
|
+
.map(([k, v]) => (v.required ? k : undefined))
|
|
98
|
+
.filter((v): v is string => v !== undefined),
|
|
99
|
+
} as InputSchemaType;
|
|
100
|
+
}
|
|
101
|
+
export function ToolArgsToParameterList<
|
|
102
|
+
TParams extends readonly NamedParam<string, ParamDef>[] = [],
|
|
103
|
+
>(toolArgs: Record<string, any>, params: TParams): (any | undefined)[] {
|
|
104
|
+
let res: any[] = [];
|
|
105
|
+
for (const param of params) {
|
|
106
|
+
res.push(toolArgs[param.name] ?? param.default ?? undefined);
|
|
107
|
+
}
|
|
108
|
+
return res;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type ToolDefinitionCreator<
|
|
112
|
+
ReferencedServerType,
|
|
113
|
+
TParams extends readonly NamedParam<string, ParamDef>[] = [],
|
|
114
|
+
> = <UserRef extends ReferencedServerType, TExtra = any>(
|
|
115
|
+
callback: (
|
|
116
|
+
...args: [UserRef, ...NamedParamsToTuple<TParams>, TExtra?]
|
|
117
|
+
) => Promise<CallToolResult>,
|
|
118
|
+
) => any;
|
|
119
|
+
|
|
120
|
+
export function ToolDefinition<
|
|
121
|
+
ReferencedServerType = any,
|
|
122
|
+
TParams extends readonly NamedParam<string, ParamDef>[] = [],
|
|
123
|
+
>(methodName: string, methodDescription: string, params: TParams): ToolDefinitionCreator<ReferencedServerType, TParams> {
|
|
124
|
+
return <UserRef extends ReferencedServerType, TExtra = any>(
|
|
125
|
+
callback: (
|
|
126
|
+
...args: [UserRef, ...NamedParamsToTuple<TParams>, TExtra?]
|
|
127
|
+
) => Promise<CallToolResult>,
|
|
128
|
+
) => {
|
|
129
|
+
return {
|
|
130
|
+
name: methodName,
|
|
131
|
+
description: methodDescription,
|
|
132
|
+
params,
|
|
133
|
+
callback,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ToolDefinitionGeneric<
|
|
139
|
+
ReferencedServerType = any,
|
|
140
|
+
// TParams extends readonly NamedParam<string, ParamDef>[] = [],
|
|
141
|
+
TParams = any,
|
|
142
|
+
> = {
|
|
143
|
+
name: string;
|
|
144
|
+
description: string;
|
|
145
|
+
params: TParams;
|
|
146
|
+
callback: (
|
|
147
|
+
argfirst: ReferencedServerType,
|
|
148
|
+
...args: any[]
|
|
149
|
+
) => Promise<CallToolResult>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export interface ToolResult {
|
|
153
|
+
content: PropertyDescription[]; // TODO: maybe change
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface Tool {
|
|
157
|
+
name: string;
|
|
158
|
+
description: string;
|
|
159
|
+
inputSchema: InputSchemaType;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ToolSet {
|
|
163
|
+
getTools(): ToolDefinitionGeneric[];
|
|
164
|
+
getTool(toolName: string): ToolDefinitionGeneric | undefined;
|
|
165
|
+
}
|
package/test/mcp.test.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it, test } from "bun:test";
|
|
5
|
+
import { MCPServerService } from "../src";
|
|
6
|
+
import { TestToolSet } from "./testToolSet";
|
|
7
|
+
|
|
8
|
+
describe("MCPServerService", () => {
|
|
9
|
+
const serverService = new MCPServerService(
|
|
10
|
+
"testServerTitle",
|
|
11
|
+
"1.0.0",
|
|
12
|
+
"No instructions",
|
|
13
|
+
(toolName: string) => { return serverService },
|
|
14
|
+
[
|
|
15
|
+
new TestToolSet(),
|
|
16
|
+
]
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
test("that the server has the tools", async () => {
|
|
20
|
+
const toolsListResult = await serverService["tools/list"]({});
|
|
21
|
+
expect(toolsListResult.tools).toBeArrayOfSize(2);
|
|
22
|
+
expect(toolsListResult.tools.at(0)?.name).toBe("tool_name_1");
|
|
23
|
+
expect(toolsListResult.tools.at(1)?.name).toBe("tool_name_2");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("that the server has callable tools that works without args", async () => {
|
|
27
|
+
const toolsListResult = await serverService["tools/call"]({
|
|
28
|
+
name: "tool_name_1"
|
|
29
|
+
});
|
|
30
|
+
expect(toolsListResult.isError).toBeFalse();
|
|
31
|
+
expect(toolsListResult.content).toBeArrayOfSize(1)
|
|
32
|
+
expect(toolsListResult.content[0]).toBeObject();
|
|
33
|
+
expect(toolsListResult.content[0]?.type).toBe("text");
|
|
34
|
+
expect(toolsListResult.content[0]?.text).toBe(undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("that the server has callable tools that works with args", async () => {
|
|
38
|
+
const toolsListResult = await serverService["tools/call"]({
|
|
39
|
+
name: "tool_name_1",
|
|
40
|
+
arguments: {
|
|
41
|
+
"arg1": "any"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
expect(toolsListResult.isError).toBeFalse();
|
|
45
|
+
expect(toolsListResult.content).toBeArrayOfSize(1)
|
|
46
|
+
expect(toolsListResult.content[0]).toBeObject();
|
|
47
|
+
expect(toolsListResult.content[0]?.type).toBe("text");
|
|
48
|
+
expect(toolsListResult.content[0]?.text).toBe("any");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("that the server has callable tools that works with arg name validation", async () => {
|
|
52
|
+
const toolsListResult = await serverService["tools/call"]({
|
|
53
|
+
name: "tool_name_1",
|
|
54
|
+
arguments: {
|
|
55
|
+
"arg0": "any"
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
expect(toolsListResult.isError).toBeFalse();
|
|
59
|
+
expect(toolsListResult.content).toBeArrayOfSize(1)
|
|
60
|
+
expect(toolsListResult.content[0]).toBeObject();
|
|
61
|
+
expect(toolsListResult.content[0]?.type).toBe("text");
|
|
62
|
+
expect(toolsListResult.content[0]?.text).toBe(undefined); // since we dont have the correct argument name
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ToolDefinition,
|
|
3
|
+
type CallToolResult,
|
|
4
|
+
type ToolDefinitionGeneric,
|
|
5
|
+
type ToolSet,
|
|
6
|
+
} from "../src/types";
|
|
7
|
+
|
|
8
|
+
export const Tool1 = ToolDefinition(
|
|
9
|
+
"tool_name_1",
|
|
10
|
+
"this tool is a test tool 1",
|
|
11
|
+
[
|
|
12
|
+
{
|
|
13
|
+
name: "arg1",
|
|
14
|
+
description: "describing arg1",
|
|
15
|
+
type: "string",
|
|
16
|
+
required: false,
|
|
17
|
+
},
|
|
18
|
+
] as const,
|
|
19
|
+
)(async (
|
|
20
|
+
_reference: object,
|
|
21
|
+
arg_string: string,
|
|
22
|
+
_extra?: any,
|
|
23
|
+
): Promise<CallToolResult> => {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: arg_string,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const Tool2 = ToolDefinition(
|
|
35
|
+
"tool_name_2",
|
|
36
|
+
"this tool is a test tool 2",
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
name: "arg1",
|
|
40
|
+
description: "describing arg1",
|
|
41
|
+
type: "string",
|
|
42
|
+
required: false,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "arg2",
|
|
46
|
+
description: "describing arg2",
|
|
47
|
+
type: "boolean",
|
|
48
|
+
required: false,
|
|
49
|
+
},
|
|
50
|
+
] as const,
|
|
51
|
+
)(async (
|
|
52
|
+
_reference: any,
|
|
53
|
+
arg1: string,
|
|
54
|
+
arg2: boolean,
|
|
55
|
+
_extra?: any,
|
|
56
|
+
): Promise<CallToolResult> => {
|
|
57
|
+
throw new Error("this always fails");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export class TestToolSet implements ToolSet {
|
|
61
|
+
getTools(): ToolDefinitionGeneric[] {
|
|
62
|
+
return [Tool1, Tool2];
|
|
63
|
+
}
|
|
64
|
+
getTool(toolName: string): ToolDefinitionGeneric | undefined {
|
|
65
|
+
return this.getTools().find(x => x.name == toolName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it, test } from "bun:test";
|
|
2
|
+
import { TestToolSet } from "./testToolSet";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe("TestToolSet", () => {
|
|
6
|
+
const toolSet = new TestToolSet();
|
|
7
|
+
test("that the toolset can handle any name", async () => {
|
|
8
|
+
expect(toolSet.getTool("tool_name_1")).not.toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
const tools = toolSet.getTools();
|
|
11
|
+
for (const toolId in tools) {
|
|
12
|
+
const tool = tools[toolId];
|
|
13
|
+
if (!tool) continue;
|
|
14
|
+
test(`that the toolset ${toolId} is constructed correctly`, async () => {
|
|
15
|
+
expect(tool.name).toBeOneOf(["tool_name_1", "tool_name_2"]);
|
|
16
|
+
expect(tool.description).not.toBe("");
|
|
17
|
+
expect(tool.params).toBeArray();
|
|
18
|
+
expect(tool.params).not.toBeArrayOfSize(0);
|
|
19
|
+
expect(tool.params.length).toBeOneOf([1, 2]);
|
|
20
|
+
});
|
|
21
|
+
// test(`that the toolset ${i} is callable with any amount of args`, async () => {
|
|
22
|
+
const callback = tool.callback;
|
|
23
|
+
const args = [this, "test", false, "test"];
|
|
24
|
+
|
|
25
|
+
for (let n = 0; n < args.length + 1; n++) {
|
|
26
|
+
test(`should handle ${n} args correctly `, async () => {
|
|
27
|
+
const result = await expect(async () => {
|
|
28
|
+
const xargs = args.slice(0, n);
|
|
29
|
+
const ret = await (callback as any)(...xargs);
|
|
30
|
+
expect(ret).toBeObject();
|
|
31
|
+
expect(ret.content).toBeArrayOfSize(1);
|
|
32
|
+
if(n > 1) expect(ret.content[0].text).toBe(args[1]);
|
|
33
|
+
});
|
|
34
|
+
if (toolId == "0") {
|
|
35
|
+
result.not.toThrow();
|
|
36
|
+
} else if (toolId == "1") {
|
|
37
|
+
result.toThrow("this always fails");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"emitDeclarationOnly": true,
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"noCheck": true,
|
|
9
|
+
},
|
|
10
|
+
"exclude": ["node_modules", "test", "src/**/*.d.ts"],
|
|
11
|
+
"include": ["src/**/*.ts"]
|
|
12
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": false,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationDir": "./out",
|
|
18
|
+
"emitDeclarationOnly": true,
|
|
19
|
+
"rootDir": "./src",
|
|
20
|
+
|
|
21
|
+
// Best practices
|
|
22
|
+
"strict": true,
|
|
23
|
+
"skipLibCheck": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedIndexedAccess": true,
|
|
26
|
+
"noImplicitOverride": true,
|
|
27
|
+
|
|
28
|
+
// Some stricter flags (disabled by default)
|
|
29
|
+
"noUnusedLocals": false,
|
|
30
|
+
"noUnusedParameters": false,
|
|
31
|
+
"noPropertyAccessFromIndexSignature": false
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*.ts"
|
|
35
|
+
]
|
|
36
|
+
}
|