@universal-mcp-toolkit/core 0.1.0
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 +21 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +588 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 universal-mcp-toolkit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { McpServer, PromptCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { ReadResourceResult, GetPromptResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { Logger } from 'pino';
|
|
4
|
+
import { ZodRawShapeCompat, ShapeOutput } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
declare abstract class ToolkitServer {
|
|
8
|
+
readonly metadata: ToolkitServerMetadata;
|
|
9
|
+
readonly logger: Logger;
|
|
10
|
+
readonly server: McpServer;
|
|
11
|
+
private readonly tools;
|
|
12
|
+
private readonly resources;
|
|
13
|
+
private readonly prompts;
|
|
14
|
+
protected constructor(metadata: ToolkitServerMetadata, logger?: Logger);
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
getToolNames(): readonly string[];
|
|
17
|
+
getResourceNames(): readonly string[];
|
|
18
|
+
getPromptNames(): readonly string[];
|
|
19
|
+
invokeTool<TOutput>(name: string, input: unknown, sessionId?: string): Promise<TOutput>;
|
|
20
|
+
protected registerTool<TInputShape extends ZodShape, TOutputShape extends ZodShape>(definition: ToolkitToolDefinition<TInputShape, TOutputShape>): void;
|
|
21
|
+
protected registerStaticResource(name: string, uri: string, config: ToolkitResourceConfig, read: ToolkitStaticResourceHandler): void;
|
|
22
|
+
protected registerTemplateResource(name: string, template: string, config: ToolkitResourceConfig, read: ToolkitTemplateResourceHandler): void;
|
|
23
|
+
protected registerPrompt<TArgs extends ZodShape>(name: string, config: ToolkitPromptConfig<TArgs>, handler: ToolkitPromptHandler<TArgs>): void;
|
|
24
|
+
protected createJsonResource(uri: string, payload: unknown): ReadResourceResult;
|
|
25
|
+
protected createTextPrompt(text: string): GetPromptResult;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ToolkitTransport = "stdio" | "sse";
|
|
29
|
+
type ToolkitLogLevel = "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
|
|
30
|
+
type ZodShape = ZodRawShapeCompat;
|
|
31
|
+
type InferShape<TShape extends ZodShape> = ShapeOutput<TShape>;
|
|
32
|
+
interface ToolkitServerMetadata {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description: string;
|
|
36
|
+
version: string;
|
|
37
|
+
packageName: string;
|
|
38
|
+
homepage: string;
|
|
39
|
+
repositoryUrl?: string;
|
|
40
|
+
documentationUrl?: string;
|
|
41
|
+
envVarNames: readonly string[];
|
|
42
|
+
transports: readonly ToolkitTransport[];
|
|
43
|
+
toolNames: readonly string[];
|
|
44
|
+
resourceNames: readonly string[];
|
|
45
|
+
promptNames: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
interface ToolkitServerCard {
|
|
48
|
+
name: string;
|
|
49
|
+
title: string;
|
|
50
|
+
description: string;
|
|
51
|
+
version: string;
|
|
52
|
+
packageName: string;
|
|
53
|
+
homepage: string;
|
|
54
|
+
repositoryUrl?: string;
|
|
55
|
+
documentationUrl?: string;
|
|
56
|
+
transports: readonly ToolkitTransport[];
|
|
57
|
+
authentication: {
|
|
58
|
+
mode: "environment-variables";
|
|
59
|
+
required: readonly string[];
|
|
60
|
+
};
|
|
61
|
+
capabilities: {
|
|
62
|
+
tools: boolean;
|
|
63
|
+
resources: boolean;
|
|
64
|
+
prompts: boolean;
|
|
65
|
+
};
|
|
66
|
+
tools: readonly string[];
|
|
67
|
+
resources: readonly string[];
|
|
68
|
+
prompts: readonly string[];
|
|
69
|
+
}
|
|
70
|
+
interface ToolkitRuntimeOptions {
|
|
71
|
+
transport: ToolkitTransport;
|
|
72
|
+
host: string;
|
|
73
|
+
port: number;
|
|
74
|
+
ssePath: string;
|
|
75
|
+
messagesPath: string;
|
|
76
|
+
wellKnownPath: string;
|
|
77
|
+
healthPath: string;
|
|
78
|
+
}
|
|
79
|
+
interface ToolkitToolExecutionContext {
|
|
80
|
+
logger: Logger;
|
|
81
|
+
sessionId?: string;
|
|
82
|
+
log: (level: ToolkitLogLevel, message: string) => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
interface ToolkitToolDefinition<TInputShape extends ZodShape, TOutputShape extends ZodShape> {
|
|
85
|
+
name: string;
|
|
86
|
+
title?: string;
|
|
87
|
+
description: string;
|
|
88
|
+
inputSchema: TInputShape;
|
|
89
|
+
outputSchema: TOutputShape;
|
|
90
|
+
annotations?: ToolAnnotations;
|
|
91
|
+
handler: (input: InferShape<TInputShape>, context: ToolkitToolExecutionContext) => Promise<InferShape<TOutputShape>>;
|
|
92
|
+
renderText?: (output: InferShape<TOutputShape>) => string;
|
|
93
|
+
}
|
|
94
|
+
interface ToolkitResourceConfig {
|
|
95
|
+
title?: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
mimeType?: string;
|
|
98
|
+
}
|
|
99
|
+
interface ToolkitPromptConfig<TArgs extends ZodShape> {
|
|
100
|
+
title?: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
argsSchema: TArgs;
|
|
103
|
+
}
|
|
104
|
+
type ToolkitPromptHandler<TArgs extends ZodShape> = PromptCallback<TArgs>;
|
|
105
|
+
type ToolkitStaticResourceHandler = (uri: URL) => Promise<ReadResourceResult> | ReadResourceResult;
|
|
106
|
+
type ToolkitTemplateResourceHandler = (uri: URL, params: Record<string, string | string[]>) => Promise<ReadResourceResult> | ReadResourceResult;
|
|
107
|
+
interface ToolkitRuntimeRegistration {
|
|
108
|
+
createServer: () => Promise<ToolkitServer> | ToolkitServer;
|
|
109
|
+
serverCard: ToolkitServerCard;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
declare function createServerCard(metadata: ToolkitServerMetadata): ToolkitServerCard;
|
|
113
|
+
|
|
114
|
+
declare function loadEnv<TShape extends z.ZodRawShape>(shape: TShape, source?: NodeJS.ProcessEnv): z.infer<z.ZodObject<TShape>>;
|
|
115
|
+
|
|
116
|
+
interface ToolkitErrorOptions {
|
|
117
|
+
code: string;
|
|
118
|
+
statusCode?: number;
|
|
119
|
+
details?: unknown;
|
|
120
|
+
exposeToClient?: boolean;
|
|
121
|
+
}
|
|
122
|
+
declare class ToolkitError extends Error {
|
|
123
|
+
readonly code: string;
|
|
124
|
+
readonly statusCode: number;
|
|
125
|
+
readonly details?: unknown;
|
|
126
|
+
readonly exposeToClient: boolean;
|
|
127
|
+
constructor(message: string, options: ToolkitErrorOptions);
|
|
128
|
+
toClientMessage(): string;
|
|
129
|
+
}
|
|
130
|
+
declare class ConfigurationError extends ToolkitError {
|
|
131
|
+
constructor(message: string, details?: unknown);
|
|
132
|
+
}
|
|
133
|
+
declare class ValidationError extends ToolkitError {
|
|
134
|
+
constructor(message: string, details?: unknown);
|
|
135
|
+
}
|
|
136
|
+
declare class ExternalServiceError extends ToolkitError {
|
|
137
|
+
constructor(message: string, options?: Omit<ToolkitErrorOptions, "code">);
|
|
138
|
+
}
|
|
139
|
+
declare function normalizeError(error: unknown): ToolkitError;
|
|
140
|
+
|
|
141
|
+
type QueryValue = string | number | boolean | null | undefined;
|
|
142
|
+
interface HttpServiceClientOptions {
|
|
143
|
+
serviceName: string;
|
|
144
|
+
baseUrl: string;
|
|
145
|
+
logger: Logger;
|
|
146
|
+
defaultHeaders?: HeadersInit | (() => HeadersInit);
|
|
147
|
+
}
|
|
148
|
+
interface HttpRequestOptions {
|
|
149
|
+
method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
150
|
+
headers?: HeadersInit;
|
|
151
|
+
query?: Record<string, QueryValue>;
|
|
152
|
+
body?: BodyInit | object;
|
|
153
|
+
}
|
|
154
|
+
declare class HttpServiceClient {
|
|
155
|
+
protected readonly baseUrl: string;
|
|
156
|
+
protected readonly logger: Logger;
|
|
157
|
+
private readonly serviceName;
|
|
158
|
+
private readonly defaultHeaders;
|
|
159
|
+
constructor(options: HttpServiceClientOptions);
|
|
160
|
+
getJson<T>(path: string, schema: z.ZodType<T>, options?: Omit<HttpRequestOptions, "method">): Promise<T>;
|
|
161
|
+
postJson<T>(path: string, schema: z.ZodType<T>, options?: Omit<HttpRequestOptions, "method">): Promise<T>;
|
|
162
|
+
requestJson<T>(path: string, schema: z.ZodType<T>, options?: HttpRequestOptions): Promise<T>;
|
|
163
|
+
requestText(path: string, options?: HttpRequestOptions): Promise<string>;
|
|
164
|
+
protected fetch(path: string, options?: HttpRequestOptions): Promise<Response>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface LoggerOptions {
|
|
168
|
+
name: string;
|
|
169
|
+
level?: string;
|
|
170
|
+
}
|
|
171
|
+
declare function createLogger(options: LoggerOptions): Logger;
|
|
172
|
+
|
|
173
|
+
declare function parseRuntimeOptions(argv?: readonly string[]): ToolkitRuntimeOptions;
|
|
174
|
+
declare function runToolkitServer(registration: ToolkitRuntimeRegistration, options?: ToolkitRuntimeOptions): Promise<void>;
|
|
175
|
+
|
|
176
|
+
declare function defineTool<TInputShape extends ZodShape, TOutputShape extends ZodShape>(definition: ToolkitToolDefinition<TInputShape, TOutputShape>): ToolkitToolDefinition<TInputShape, TOutputShape>;
|
|
177
|
+
|
|
178
|
+
export { ConfigurationError, ExternalServiceError, HttpServiceClient, type InferShape, ToolkitError, type ToolkitLogLevel, type ToolkitPromptConfig, type ToolkitPromptHandler, type ToolkitResourceConfig, type ToolkitRuntimeOptions, type ToolkitRuntimeRegistration, ToolkitServer, type ToolkitServerCard, type ToolkitServerMetadata, type ToolkitStaticResourceHandler, type ToolkitTemplateResourceHandler, type ToolkitToolDefinition, type ToolkitToolExecutionContext, type ToolkitTransport, ValidationError, type ZodShape, createLogger, createServerCard, defineTool, loadEnv, normalizeError, parseRuntimeOptions, runToolkitServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
// src/card.ts
|
|
2
|
+
function createServerCard(metadata) {
|
|
3
|
+
const card = {
|
|
4
|
+
name: metadata.id,
|
|
5
|
+
title: metadata.title,
|
|
6
|
+
description: metadata.description,
|
|
7
|
+
version: metadata.version,
|
|
8
|
+
packageName: metadata.packageName,
|
|
9
|
+
homepage: metadata.homepage,
|
|
10
|
+
transports: metadata.transports,
|
|
11
|
+
authentication: {
|
|
12
|
+
mode: "environment-variables",
|
|
13
|
+
required: metadata.envVarNames
|
|
14
|
+
},
|
|
15
|
+
capabilities: {
|
|
16
|
+
tools: metadata.toolNames.length > 0,
|
|
17
|
+
resources: metadata.resourceNames.length > 0,
|
|
18
|
+
prompts: metadata.promptNames.length > 0
|
|
19
|
+
},
|
|
20
|
+
tools: metadata.toolNames,
|
|
21
|
+
resources: metadata.resourceNames,
|
|
22
|
+
prompts: metadata.promptNames
|
|
23
|
+
};
|
|
24
|
+
if (metadata.repositoryUrl) {
|
|
25
|
+
card.repositoryUrl = metadata.repositoryUrl;
|
|
26
|
+
}
|
|
27
|
+
if (metadata.documentationUrl) {
|
|
28
|
+
card.documentationUrl = metadata.documentationUrl;
|
|
29
|
+
}
|
|
30
|
+
return card;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/env.ts
|
|
34
|
+
import dotenv from "dotenv";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
// src/errors.ts
|
|
38
|
+
var ToolkitError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
statusCode;
|
|
41
|
+
details;
|
|
42
|
+
exposeToClient;
|
|
43
|
+
constructor(message, options) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "ToolkitError";
|
|
46
|
+
this.code = options.code;
|
|
47
|
+
this.statusCode = options.statusCode ?? 500;
|
|
48
|
+
this.details = options.details;
|
|
49
|
+
this.exposeToClient = options.exposeToClient ?? true;
|
|
50
|
+
}
|
|
51
|
+
toClientMessage() {
|
|
52
|
+
return this.exposeToClient ? this.message : "The upstream service returned an unexpected error.";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var ConfigurationError = class extends ToolkitError {
|
|
56
|
+
constructor(message, details) {
|
|
57
|
+
super(message, {
|
|
58
|
+
code: "configuration_error",
|
|
59
|
+
statusCode: 500,
|
|
60
|
+
details,
|
|
61
|
+
exposeToClient: true
|
|
62
|
+
});
|
|
63
|
+
this.name = "ConfigurationError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ValidationError = class extends ToolkitError {
|
|
67
|
+
constructor(message, details) {
|
|
68
|
+
super(message, {
|
|
69
|
+
code: "validation_error",
|
|
70
|
+
statusCode: 400,
|
|
71
|
+
details,
|
|
72
|
+
exposeToClient: true
|
|
73
|
+
});
|
|
74
|
+
this.name = "ValidationError";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var ExternalServiceError = class extends ToolkitError {
|
|
78
|
+
constructor(message, options) {
|
|
79
|
+
super(message, {
|
|
80
|
+
code: "external_service_error",
|
|
81
|
+
statusCode: options?.statusCode ?? 502,
|
|
82
|
+
details: options?.details,
|
|
83
|
+
exposeToClient: options?.exposeToClient ?? true
|
|
84
|
+
});
|
|
85
|
+
this.name = "ExternalServiceError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function normalizeError(error) {
|
|
89
|
+
if (error instanceof ToolkitError) {
|
|
90
|
+
return error;
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
return new ToolkitError(error.message, {
|
|
94
|
+
code: "unexpected_error",
|
|
95
|
+
statusCode: 500,
|
|
96
|
+
details: error.stack,
|
|
97
|
+
exposeToClient: true
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return new ToolkitError("An unexpected non-error value was thrown.", {
|
|
101
|
+
code: "unexpected_error",
|
|
102
|
+
statusCode: 500,
|
|
103
|
+
details: error,
|
|
104
|
+
exposeToClient: false
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/env.ts
|
|
109
|
+
function loadEnv(shape, source = process.env) {
|
|
110
|
+
dotenv.config();
|
|
111
|
+
const schema = z.object(shape);
|
|
112
|
+
const result = schema.safeParse(source);
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
throw new ConfigurationError("Environment validation failed.", result.error.flatten());
|
|
115
|
+
}
|
|
116
|
+
return result.data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/http.ts
|
|
120
|
+
var HttpServiceClient = class {
|
|
121
|
+
baseUrl;
|
|
122
|
+
logger;
|
|
123
|
+
serviceName;
|
|
124
|
+
defaultHeaders;
|
|
125
|
+
constructor(options) {
|
|
126
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
127
|
+
this.logger = options.logger;
|
|
128
|
+
this.serviceName = options.serviceName;
|
|
129
|
+
this.defaultHeaders = options.defaultHeaders;
|
|
130
|
+
}
|
|
131
|
+
async getJson(path, schema, options = {}) {
|
|
132
|
+
return this.requestJson(path, schema, {
|
|
133
|
+
...options,
|
|
134
|
+
method: "GET"
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async postJson(path, schema, options = {}) {
|
|
138
|
+
return this.requestJson(path, schema, {
|
|
139
|
+
...options,
|
|
140
|
+
method: "POST"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async requestJson(path, schema, options = {}) {
|
|
144
|
+
const response = await this.fetch(path, options);
|
|
145
|
+
const payload = await response.json();
|
|
146
|
+
const parsed = schema.safeParse(payload);
|
|
147
|
+
if (!parsed.success) {
|
|
148
|
+
throw new ExternalServiceError(`${this.serviceName} returned a response that failed schema validation.`, {
|
|
149
|
+
details: parsed.error.flatten()
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return parsed.data;
|
|
153
|
+
}
|
|
154
|
+
async requestText(path, options = {}) {
|
|
155
|
+
const response = await this.fetch(path, options);
|
|
156
|
+
return response.text();
|
|
157
|
+
}
|
|
158
|
+
async fetch(path, options = {}) {
|
|
159
|
+
const url = new URL(path.startsWith("http") ? path : `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
160
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
161
|
+
if (value !== void 0 && value !== null) {
|
|
162
|
+
url.searchParams.set(key, String(value));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const headers = new Headers();
|
|
166
|
+
const resolvedDefaultHeaders = typeof this.defaultHeaders === "function" ? this.defaultHeaders() : this.defaultHeaders ?? {};
|
|
167
|
+
new Headers(resolvedDefaultHeaders).forEach((value, key) => headers.set(key, value));
|
|
168
|
+
new Headers(options.headers).forEach((value, key) => headers.set(key, value));
|
|
169
|
+
let body;
|
|
170
|
+
if (options.body instanceof URLSearchParams || typeof options.body === "string" || options.body instanceof ArrayBuffer || options.body instanceof Blob || options.body instanceof FormData) {
|
|
171
|
+
body = options.body;
|
|
172
|
+
} else if (options.body !== void 0) {
|
|
173
|
+
headers.set("content-type", headers.get("content-type") ?? "application/json");
|
|
174
|
+
body = JSON.stringify(options.body);
|
|
175
|
+
}
|
|
176
|
+
const requestInit = {
|
|
177
|
+
method: options.method ?? "GET",
|
|
178
|
+
headers
|
|
179
|
+
};
|
|
180
|
+
if (body !== void 0) {
|
|
181
|
+
requestInit.body = body;
|
|
182
|
+
}
|
|
183
|
+
const response = await fetch(url, requestInit);
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const text = await response.text();
|
|
186
|
+
this.logger.error(
|
|
187
|
+
{
|
|
188
|
+
service: this.serviceName,
|
|
189
|
+
status: response.status,
|
|
190
|
+
url: url.toString(),
|
|
191
|
+
body: text
|
|
192
|
+
},
|
|
193
|
+
"Upstream request failed"
|
|
194
|
+
);
|
|
195
|
+
throw new ExternalServiceError(`${this.serviceName} request failed with status ${response.status}.`, {
|
|
196
|
+
statusCode: response.status,
|
|
197
|
+
details: text
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return response;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/logger.ts
|
|
205
|
+
import pino from "pino";
|
|
206
|
+
function createLogger(options) {
|
|
207
|
+
const destination = pino.destination({
|
|
208
|
+
dest: 2,
|
|
209
|
+
sync: false
|
|
210
|
+
});
|
|
211
|
+
return pino(
|
|
212
|
+
{
|
|
213
|
+
name: options.name,
|
|
214
|
+
level: options.level ?? process.env.LOG_LEVEL ?? "info",
|
|
215
|
+
base: null,
|
|
216
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
217
|
+
},
|
|
218
|
+
destination
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/runtime.ts
|
|
223
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
224
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
225
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
226
|
+
import { parseArgs } from "util";
|
|
227
|
+
var DEFAULT_OPTIONS = {
|
|
228
|
+
transport: "stdio",
|
|
229
|
+
host: "127.0.0.1",
|
|
230
|
+
port: 3333,
|
|
231
|
+
ssePath: "/sse",
|
|
232
|
+
messagesPath: "/messages",
|
|
233
|
+
wellKnownPath: "/.well-known/mcp-server.json",
|
|
234
|
+
healthPath: "/healthz"
|
|
235
|
+
};
|
|
236
|
+
function parsePort(raw) {
|
|
237
|
+
const port = Number.parseInt(raw, 10);
|
|
238
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
239
|
+
throw new ConfigurationError(`Invalid port value '${raw}'.`);
|
|
240
|
+
}
|
|
241
|
+
return port;
|
|
242
|
+
}
|
|
243
|
+
function parseRuntimeOptions(argv = process.argv.slice(2)) {
|
|
244
|
+
const parsed = parseArgs({
|
|
245
|
+
args: [...argv],
|
|
246
|
+
options: {
|
|
247
|
+
transport: { type: "string", default: DEFAULT_OPTIONS.transport },
|
|
248
|
+
host: { type: "string", default: DEFAULT_OPTIONS.host },
|
|
249
|
+
port: { type: "string", default: String(DEFAULT_OPTIONS.port) },
|
|
250
|
+
"sse-path": { type: "string", default: DEFAULT_OPTIONS.ssePath },
|
|
251
|
+
"messages-path": { type: "string", default: DEFAULT_OPTIONS.messagesPath },
|
|
252
|
+
"well-known-path": { type: "string", default: DEFAULT_OPTIONS.wellKnownPath },
|
|
253
|
+
"health-path": { type: "string", default: DEFAULT_OPTIONS.healthPath }
|
|
254
|
+
},
|
|
255
|
+
allowPositionals: false
|
|
256
|
+
});
|
|
257
|
+
const transport = parsed.values.transport;
|
|
258
|
+
if (transport !== "stdio" && transport !== "sse") {
|
|
259
|
+
throw new ConfigurationError(`Unsupported transport '${transport}'. Expected 'stdio' or 'sse'.`);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
transport,
|
|
263
|
+
host: parsed.values.host,
|
|
264
|
+
port: parsePort(parsed.values.port),
|
|
265
|
+
ssePath: parsed.values["sse-path"],
|
|
266
|
+
messagesPath: parsed.values["messages-path"],
|
|
267
|
+
wellKnownPath: parsed.values["well-known-path"],
|
|
268
|
+
healthPath: parsed.values["health-path"]
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function runToolkitServer(registration, options = DEFAULT_OPTIONS) {
|
|
272
|
+
const runtimeLogger = createLogger({
|
|
273
|
+
name: registration.serverCard.packageName
|
|
274
|
+
});
|
|
275
|
+
if (options.transport === "stdio") {
|
|
276
|
+
const server = await registration.createServer();
|
|
277
|
+
const transport = new StdioServerTransport();
|
|
278
|
+
await server.server.connect(transport);
|
|
279
|
+
runtimeLogger.info("MCP server listening on stdio");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const app = createMcpExpressApp({ host: options.host });
|
|
283
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
284
|
+
app.get(options.wellKnownPath, (_request, response) => {
|
|
285
|
+
response.json(registration.serverCard);
|
|
286
|
+
});
|
|
287
|
+
app.get(options.healthPath, (_request, response) => {
|
|
288
|
+
response.json({
|
|
289
|
+
status: "ok",
|
|
290
|
+
name: registration.serverCard.name,
|
|
291
|
+
version: registration.serverCard.version,
|
|
292
|
+
transport: options.transport
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
app.get(options.ssePath, async (_request, response) => {
|
|
296
|
+
try {
|
|
297
|
+
const server = await registration.createServer();
|
|
298
|
+
const transport = new SSEServerTransport(options.messagesPath, response);
|
|
299
|
+
sessions.set(transport.sessionId, { server, transport });
|
|
300
|
+
transport.onclose = () => {
|
|
301
|
+
void server.close();
|
|
302
|
+
sessions.delete(transport.sessionId);
|
|
303
|
+
};
|
|
304
|
+
await server.server.connect(transport);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
runtimeLogger.error({ error }, "Failed to establish SSE session");
|
|
307
|
+
if (!response.headersSent) {
|
|
308
|
+
response.status(500).send("Failed to establish SSE session.");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
app.post(options.messagesPath, async (request, response) => {
|
|
313
|
+
const sessionId = typeof request.query.sessionId === "string" ? request.query.sessionId : void 0;
|
|
314
|
+
if (!sessionId) {
|
|
315
|
+
response.status(400).send("Missing sessionId query parameter.");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const session = sessions.get(sessionId);
|
|
319
|
+
if (!session) {
|
|
320
|
+
response.status(404).send("Unknown sessionId.");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
await session.transport.handlePostMessage(request, response, request.body);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
runtimeLogger.error({ error, sessionId }, "Failed to handle incoming SSE message");
|
|
327
|
+
if (!response.headersSent) {
|
|
328
|
+
response.status(500).send("Failed to process request.");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
const listener = app.listen(options.port, options.host, () => {
|
|
333
|
+
runtimeLogger.info(
|
|
334
|
+
{
|
|
335
|
+
host: options.host,
|
|
336
|
+
port: options.port,
|
|
337
|
+
ssePath: options.ssePath,
|
|
338
|
+
messagesPath: options.messagesPath
|
|
339
|
+
},
|
|
340
|
+
"MCP server listening over HTTP+SSE"
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
const shutdown = async () => {
|
|
344
|
+
runtimeLogger.info("Shutting down MCP server");
|
|
345
|
+
for (const [sessionId, session] of sessions.entries()) {
|
|
346
|
+
runtimeLogger.info({ sessionId }, "Closing active session");
|
|
347
|
+
await session.transport.close();
|
|
348
|
+
await session.server.close();
|
|
349
|
+
sessions.delete(sessionId);
|
|
350
|
+
}
|
|
351
|
+
await new Promise((resolve, reject) => {
|
|
352
|
+
listener.close((error) => {
|
|
353
|
+
if (error) {
|
|
354
|
+
reject(error);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
resolve();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
362
|
+
process.once(signal, () => {
|
|
363
|
+
void shutdown().finally(() => process.exit(0));
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/server.ts
|
|
369
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
370
|
+
import { getParseErrorMessage, normalizeObjectSchema, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
371
|
+
function toText(output) {
|
|
372
|
+
return JSON.stringify(output, null, 2);
|
|
373
|
+
}
|
|
374
|
+
function mapLogLevel(level) {
|
|
375
|
+
switch (level) {
|
|
376
|
+
case "debug":
|
|
377
|
+
return "debug";
|
|
378
|
+
case "info":
|
|
379
|
+
case "notice":
|
|
380
|
+
return "info";
|
|
381
|
+
case "warning":
|
|
382
|
+
return "warn";
|
|
383
|
+
case "error":
|
|
384
|
+
return "error";
|
|
385
|
+
case "critical":
|
|
386
|
+
case "alert":
|
|
387
|
+
case "emergency":
|
|
388
|
+
return "fatal";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
var ToolkitServer = class {
|
|
392
|
+
metadata;
|
|
393
|
+
logger;
|
|
394
|
+
server;
|
|
395
|
+
tools = /* @__PURE__ */ new Map();
|
|
396
|
+
resources = /* @__PURE__ */ new Set();
|
|
397
|
+
prompts = /* @__PURE__ */ new Set();
|
|
398
|
+
constructor(metadata, logger) {
|
|
399
|
+
this.metadata = metadata;
|
|
400
|
+
this.logger = logger ?? createLogger({ name: metadata.packageName });
|
|
401
|
+
this.server = new McpServer(
|
|
402
|
+
{
|
|
403
|
+
name: metadata.id,
|
|
404
|
+
version: metadata.version,
|
|
405
|
+
websiteUrl: metadata.homepage
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
capabilities: {
|
|
409
|
+
logging: {}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
async close() {
|
|
415
|
+
await this.server.close();
|
|
416
|
+
}
|
|
417
|
+
getToolNames() {
|
|
418
|
+
return [...this.tools.keys()].sort();
|
|
419
|
+
}
|
|
420
|
+
getResourceNames() {
|
|
421
|
+
return [...this.resources].sort();
|
|
422
|
+
}
|
|
423
|
+
getPromptNames() {
|
|
424
|
+
return [...this.prompts].sort();
|
|
425
|
+
}
|
|
426
|
+
async invokeTool(name, input, sessionId) {
|
|
427
|
+
const tool = this.tools.get(name);
|
|
428
|
+
if (!tool) {
|
|
429
|
+
throw new Error(`Tool '${name}' is not registered.`);
|
|
430
|
+
}
|
|
431
|
+
return await tool.invoke(input, sessionId);
|
|
432
|
+
}
|
|
433
|
+
registerTool(definition) {
|
|
434
|
+
const inputSchema = normalizeObjectSchema(definition.inputSchema);
|
|
435
|
+
const outputSchema = normalizeObjectSchema(definition.outputSchema);
|
|
436
|
+
if (!inputSchema || !outputSchema) {
|
|
437
|
+
throw new ValidationError(`Tool '${definition.name}' requires both input and output schemas.`);
|
|
438
|
+
}
|
|
439
|
+
const storedTool = {
|
|
440
|
+
name: definition.name,
|
|
441
|
+
invoke: async (input, sessionId) => {
|
|
442
|
+
const parsedInputResult = await safeParseAsync(inputSchema, input);
|
|
443
|
+
if (!parsedInputResult.success) {
|
|
444
|
+
throw new ValidationError(
|
|
445
|
+
`Input validation failed for tool '${definition.name}': ${getParseErrorMessage(parsedInputResult.error)}`,
|
|
446
|
+
parsedInputResult.error
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const context = {
|
|
450
|
+
logger: this.logger.child({ tool: definition.name }),
|
|
451
|
+
log: async (level, message) => {
|
|
452
|
+
this.logger[mapLogLevel(level)]({ sessionId, tool: definition.name }, message);
|
|
453
|
+
if (this.server.isConnected()) {
|
|
454
|
+
if (sessionId === void 0) {
|
|
455
|
+
await this.server.sendLoggingMessage({ level, data: message });
|
|
456
|
+
} else {
|
|
457
|
+
await this.server.sendLoggingMessage({ level, data: message }, sessionId);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
if (sessionId !== void 0) {
|
|
463
|
+
context.sessionId = sessionId;
|
|
464
|
+
}
|
|
465
|
+
const output = await definition.handler(parsedInputResult.data, context);
|
|
466
|
+
const parsedOutputResult = await safeParseAsync(outputSchema, output);
|
|
467
|
+
if (!parsedOutputResult.success) {
|
|
468
|
+
throw new ValidationError(
|
|
469
|
+
`Output validation failed for tool '${definition.name}': ${getParseErrorMessage(parsedOutputResult.error)}`,
|
|
470
|
+
parsedOutputResult.error
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return parsedOutputResult.data;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const renderText = definition.renderText;
|
|
477
|
+
if (renderText) {
|
|
478
|
+
storedTool.renderText = (output) => renderText(output);
|
|
479
|
+
}
|
|
480
|
+
this.tools.set(definition.name, storedTool);
|
|
481
|
+
const toolConfig = {
|
|
482
|
+
description: definition.description,
|
|
483
|
+
inputSchema: definition.inputSchema,
|
|
484
|
+
outputSchema: definition.outputSchema
|
|
485
|
+
};
|
|
486
|
+
if (definition.title) {
|
|
487
|
+
toolConfig.title = definition.title;
|
|
488
|
+
}
|
|
489
|
+
if (definition.annotations) {
|
|
490
|
+
toolConfig.annotations = definition.annotations;
|
|
491
|
+
}
|
|
492
|
+
const toolCallback = (async (input, extra) => {
|
|
493
|
+
try {
|
|
494
|
+
const output = await this.invokeTool(definition.name, input, extra.sessionId);
|
|
495
|
+
return {
|
|
496
|
+
content: [
|
|
497
|
+
{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: definition.renderText ? definition.renderText(output) : toText(output)
|
|
500
|
+
}
|
|
501
|
+
],
|
|
502
|
+
structuredContent: output
|
|
503
|
+
};
|
|
504
|
+
} catch (error) {
|
|
505
|
+
const normalized = normalizeError(error);
|
|
506
|
+
this.logger.error(
|
|
507
|
+
{
|
|
508
|
+
tool: definition.name,
|
|
509
|
+
code: normalized.code,
|
|
510
|
+
details: normalized.details
|
|
511
|
+
},
|
|
512
|
+
normalized.message
|
|
513
|
+
);
|
|
514
|
+
return {
|
|
515
|
+
isError: true,
|
|
516
|
+
content: [
|
|
517
|
+
{
|
|
518
|
+
type: "text",
|
|
519
|
+
text: normalized.toClientMessage()
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
this.server.registerTool(definition.name, toolConfig, toolCallback);
|
|
526
|
+
}
|
|
527
|
+
registerStaticResource(name, uri, config, read) {
|
|
528
|
+
this.resources.add(name);
|
|
529
|
+
this.server.registerResource(name, uri, config, read);
|
|
530
|
+
}
|
|
531
|
+
registerTemplateResource(name, template, config, read) {
|
|
532
|
+
this.resources.add(name);
|
|
533
|
+
this.server.registerResource(
|
|
534
|
+
name,
|
|
535
|
+
new ResourceTemplate(template, { list: void 0 }),
|
|
536
|
+
config,
|
|
537
|
+
(uri, variables) => read(uri, variables)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
registerPrompt(name, config, handler) {
|
|
541
|
+
this.prompts.add(name);
|
|
542
|
+
this.server.registerPrompt(name, config, handler);
|
|
543
|
+
}
|
|
544
|
+
createJsonResource(uri, payload) {
|
|
545
|
+
return {
|
|
546
|
+
contents: [
|
|
547
|
+
{
|
|
548
|
+
uri,
|
|
549
|
+
mimeType: "application/json",
|
|
550
|
+
text: JSON.stringify(payload, null, 2)
|
|
551
|
+
}
|
|
552
|
+
]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
createTextPrompt(text) {
|
|
556
|
+
return {
|
|
557
|
+
messages: [
|
|
558
|
+
{
|
|
559
|
+
role: "user",
|
|
560
|
+
content: {
|
|
561
|
+
type: "text",
|
|
562
|
+
text
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/tool.ts
|
|
571
|
+
function defineTool(definition) {
|
|
572
|
+
return definition;
|
|
573
|
+
}
|
|
574
|
+
export {
|
|
575
|
+
ConfigurationError,
|
|
576
|
+
ExternalServiceError,
|
|
577
|
+
HttpServiceClient,
|
|
578
|
+
ToolkitError,
|
|
579
|
+
ToolkitServer,
|
|
580
|
+
ValidationError,
|
|
581
|
+
createLogger,
|
|
582
|
+
createServerCard,
|
|
583
|
+
defineTool,
|
|
584
|
+
loadEnv,
|
|
585
|
+
normalizeError,
|
|
586
|
+
parseRuntimeOptions,
|
|
587
|
+
runToolkitServer
|
|
588
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@universal-mcp-toolkit/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared runtime, validation, logging, transport, and DX primitives for universal-mcp-toolkit.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Markgatcha/universal-mcp-toolkit.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Markgatcha/universal-mcp-toolkit#readme",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"ai",
|
|
26
|
+
"developer-tools",
|
|
27
|
+
"typescript",
|
|
28
|
+
"core",
|
|
29
|
+
"sdk",
|
|
30
|
+
"transports",
|
|
31
|
+
"zod",
|
|
32
|
+
"pino"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
36
|
+
"dotenv": "^17.3.1",
|
|
37
|
+
"express": "^5.2.1",
|
|
38
|
+
"pino": "^10.3.1",
|
|
39
|
+
"zod": "^4.3.6"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
46
|
+
"dev": "tsx watch src/index.ts",
|
|
47
|
+
"lint": "tsc --noEmit",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run --passWithNoTests",
|
|
50
|
+
"clean": "rimraf dist"
|
|
51
|
+
}
|
|
52
|
+
}
|