@universal-mcp-toolkit/core 0.1.0 → 0.2.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 -21
- package/dist/index.d.ts +44 -2
- package/dist/index.js +181 -13
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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
CHANGED
|
@@ -88,7 +88,13 @@ interface ToolkitToolDefinition<TInputShape extends ZodShape, TOutputShape exten
|
|
|
88
88
|
inputSchema: TInputShape;
|
|
89
89
|
outputSchema: TOutputShape;
|
|
90
90
|
annotations?: ToolAnnotations;
|
|
91
|
-
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
/**
|
|
93
|
+
* @experimental - Streaming MCP tool responses are not yet widely supported by MCP clients.
|
|
94
|
+
* Enable only if your host client explicitly supports streaming tool content.
|
|
95
|
+
*/
|
|
96
|
+
experimental_streamingResponse?: boolean;
|
|
97
|
+
handler: (input: InferShape<TInputShape>, context: ToolkitToolExecutionContext) => Promise<InferShape<TOutputShape>> | AsyncIterable<string>;
|
|
92
98
|
renderText?: (output: InferShape<TOutputShape>) => string;
|
|
93
99
|
}
|
|
94
100
|
interface ToolkitResourceConfig {
|
|
@@ -137,13 +143,41 @@ declare class ExternalServiceError extends ToolkitError {
|
|
|
137
143
|
constructor(message: string, options?: Omit<ToolkitErrorOptions, "code">);
|
|
138
144
|
}
|
|
139
145
|
declare function normalizeError(error: unknown): ToolkitError;
|
|
146
|
+
declare class ToolTimeoutError extends ToolkitError {
|
|
147
|
+
readonly toolName: string;
|
|
148
|
+
readonly timeoutMs: number;
|
|
149
|
+
constructor(toolName: string, timeoutMs: number);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface RateLimiterOptions {
|
|
153
|
+
requestsPerSecond: number;
|
|
154
|
+
burstLimit: number;
|
|
155
|
+
}
|
|
156
|
+
declare class RateLimiter {
|
|
157
|
+
private readonly intervalMs;
|
|
158
|
+
private readonly maxTokens;
|
|
159
|
+
private tokens;
|
|
160
|
+
private lastRefill;
|
|
161
|
+
private readonly waitQueue;
|
|
162
|
+
constructor(options: RateLimiterOptions);
|
|
163
|
+
acquire(): Promise<void>;
|
|
164
|
+
private refill;
|
|
165
|
+
}
|
|
140
166
|
|
|
141
167
|
type QueryValue = string | number | boolean | null | undefined;
|
|
168
|
+
interface RetryOptions {
|
|
169
|
+
maxRetries: number;
|
|
170
|
+
baseDelayMs: number;
|
|
171
|
+
maxDelayMs: number;
|
|
172
|
+
retryOn: number[];
|
|
173
|
+
}
|
|
142
174
|
interface HttpServiceClientOptions {
|
|
143
175
|
serviceName: string;
|
|
144
176
|
baseUrl: string;
|
|
145
177
|
logger: Logger;
|
|
146
178
|
defaultHeaders?: HeadersInit | (() => HeadersInit);
|
|
179
|
+
retryOptions?: Partial<RetryOptions>;
|
|
180
|
+
rateLimiter?: RateLimiter;
|
|
147
181
|
}
|
|
148
182
|
interface HttpRequestOptions {
|
|
149
183
|
method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
@@ -156,6 +190,8 @@ declare class HttpServiceClient {
|
|
|
156
190
|
protected readonly logger: Logger;
|
|
157
191
|
private readonly serviceName;
|
|
158
192
|
private readonly defaultHeaders;
|
|
193
|
+
private readonly retryOptions;
|
|
194
|
+
private readonly rateLimiter;
|
|
159
195
|
constructor(options: HttpServiceClientOptions);
|
|
160
196
|
getJson<T>(path: string, schema: z.ZodType<T>, options?: Omit<HttpRequestOptions, "method">): Promise<T>;
|
|
161
197
|
postJson<T>(path: string, schema: z.ZodType<T>, options?: Omit<HttpRequestOptions, "method">): Promise<T>;
|
|
@@ -175,4 +211,10 @@ declare function runToolkitServer(registration: ToolkitRuntimeRegistration, opti
|
|
|
175
211
|
|
|
176
212
|
declare function defineTool<TInputShape extends ZodShape, TOutputShape extends ZodShape>(definition: ToolkitToolDefinition<TInputShape, TOutputShape>): ToolkitToolDefinition<TInputShape, TOutputShape>;
|
|
177
213
|
|
|
178
|
-
|
|
214
|
+
/**
|
|
215
|
+
* @experimental - Streaming MCP tool responses are not yet widely supported by MCP clients.
|
|
216
|
+
* Enable only if your host client explicitly supports streaming tool content.
|
|
217
|
+
*/
|
|
218
|
+
declare function createStreamingResponse(iterable: AsyncIterable<string>): AsyncIterable<string>;
|
|
219
|
+
|
|
220
|
+
export { ConfigurationError, ExternalServiceError, HttpServiceClient, type InferShape, RateLimiter, type RetryOptions, ToolTimeoutError, 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, createStreamingResponse, defineTool, loadEnv, normalizeError, parseRuntimeOptions, runToolkitServer };
|
package/dist/index.js
CHANGED
|
@@ -104,6 +104,21 @@ function normalizeError(error) {
|
|
|
104
104
|
exposeToClient: false
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
|
+
var ToolTimeoutError = class extends ToolkitError {
|
|
108
|
+
toolName;
|
|
109
|
+
timeoutMs;
|
|
110
|
+
constructor(toolName, timeoutMs) {
|
|
111
|
+
super(`Tool '${toolName}' timed out after ${timeoutMs}ms.`, {
|
|
112
|
+
code: "tool_timeout",
|
|
113
|
+
statusCode: 504,
|
|
114
|
+
details: { toolName, timeoutMs },
|
|
115
|
+
exposeToClient: true
|
|
116
|
+
});
|
|
117
|
+
this.name = "ToolTimeoutError";
|
|
118
|
+
this.toolName = toolName;
|
|
119
|
+
this.timeoutMs = timeoutMs;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
107
122
|
|
|
108
123
|
// src/env.ts
|
|
109
124
|
function loadEnv(shape, source = process.env) {
|
|
@@ -117,16 +132,54 @@ function loadEnv(shape, source = process.env) {
|
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
// src/http.ts
|
|
135
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
136
|
+
maxRetries: 3,
|
|
137
|
+
baseDelayMs: 500,
|
|
138
|
+
maxDelayMs: 1e4,
|
|
139
|
+
retryOn: [429, 500, 502, 503, 504]
|
|
140
|
+
};
|
|
141
|
+
function parseRetryAfter(headerValue) {
|
|
142
|
+
if (!headerValue) return null;
|
|
143
|
+
const seconds = Number(headerValue);
|
|
144
|
+
if (!Number.isNaN(seconds)) {
|
|
145
|
+
return seconds * 1e3;
|
|
146
|
+
}
|
|
147
|
+
const dateMs = Date.parse(headerValue);
|
|
148
|
+
if (!Number.isNaN(dateMs)) {
|
|
149
|
+
const delta = dateMs - Date.now();
|
|
150
|
+
return delta > 0 ? delta : 0;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function computeBackoffDelay(attempt, options, retryAfterHeader) {
|
|
155
|
+
const retryAfter = parseRetryAfter(retryAfterHeader);
|
|
156
|
+
if (retryAfter !== null) {
|
|
157
|
+
return Math.min(retryAfter, options.maxDelayMs);
|
|
158
|
+
}
|
|
159
|
+
const exponential = options.baseDelayMs * Math.pow(2, attempt);
|
|
160
|
+
const jitter = Math.random() * options.baseDelayMs;
|
|
161
|
+
return Math.min(exponential + jitter, options.maxDelayMs);
|
|
162
|
+
}
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
120
166
|
var HttpServiceClient = class {
|
|
121
167
|
baseUrl;
|
|
122
168
|
logger;
|
|
123
169
|
serviceName;
|
|
124
170
|
defaultHeaders;
|
|
171
|
+
retryOptions;
|
|
172
|
+
rateLimiter;
|
|
125
173
|
constructor(options) {
|
|
126
174
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
127
175
|
this.logger = options.logger;
|
|
128
176
|
this.serviceName = options.serviceName;
|
|
129
177
|
this.defaultHeaders = options.defaultHeaders;
|
|
178
|
+
this.rateLimiter = options.rateLimiter;
|
|
179
|
+
this.retryOptions = {
|
|
180
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
181
|
+
...options.retryOptions
|
|
182
|
+
};
|
|
130
183
|
}
|
|
131
184
|
async getJson(path, schema, options = {}) {
|
|
132
185
|
return this.requestJson(path, schema, {
|
|
@@ -180,24 +233,53 @@ var HttpServiceClient = class {
|
|
|
180
233
|
if (body !== void 0) {
|
|
181
234
|
requestInit.body = body;
|
|
182
235
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
if (this.rateLimiter) {
|
|
237
|
+
await this.rateLimiter.acquire();
|
|
238
|
+
}
|
|
239
|
+
let lastResponse;
|
|
240
|
+
for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
|
|
241
|
+
if (attempt > 0 && this.rateLimiter) {
|
|
242
|
+
await this.rateLimiter.acquire();
|
|
243
|
+
}
|
|
244
|
+
lastResponse = await fetch(url, requestInit);
|
|
245
|
+
if (lastResponse.ok) {
|
|
246
|
+
return lastResponse;
|
|
247
|
+
}
|
|
248
|
+
const shouldRetry = this.retryOptions.retryOn.includes(lastResponse.status);
|
|
249
|
+
if (!shouldRetry || attempt >= this.retryOptions.maxRetries) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
const retryAfterHeader = lastResponse.headers.get("retry-after");
|
|
253
|
+
const delay = computeBackoffDelay(attempt, this.retryOptions, retryAfterHeader);
|
|
254
|
+
this.logger.warn(
|
|
187
255
|
{
|
|
188
256
|
service: this.serviceName,
|
|
189
|
-
status:
|
|
257
|
+
status: lastResponse.status,
|
|
190
258
|
url: url.toString(),
|
|
191
|
-
|
|
259
|
+
attempt: attempt + 1,
|
|
260
|
+
maxRetries: this.retryOptions.maxRetries,
|
|
261
|
+
delayMs: Math.round(delay)
|
|
192
262
|
},
|
|
193
|
-
"
|
|
263
|
+
"Retrying failed upstream request"
|
|
194
264
|
);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
details: text
|
|
198
|
-
});
|
|
265
|
+
await lastResponse.text().catch(() => void 0);
|
|
266
|
+
await sleep(delay);
|
|
199
267
|
}
|
|
200
|
-
|
|
268
|
+
const response = lastResponse;
|
|
269
|
+
const text = await response.text();
|
|
270
|
+
this.logger.error(
|
|
271
|
+
{
|
|
272
|
+
service: this.serviceName,
|
|
273
|
+
status: response.status,
|
|
274
|
+
url: url.toString(),
|
|
275
|
+
body: text
|
|
276
|
+
},
|
|
277
|
+
"Upstream request failed"
|
|
278
|
+
);
|
|
279
|
+
throw new ExternalServiceError(`${this.serviceName} request failed with status ${response.status}.`, {
|
|
280
|
+
statusCode: response.status,
|
|
281
|
+
details: text
|
|
282
|
+
});
|
|
201
283
|
}
|
|
202
284
|
};
|
|
203
285
|
|
|
@@ -219,6 +301,55 @@ function createLogger(options) {
|
|
|
219
301
|
);
|
|
220
302
|
}
|
|
221
303
|
|
|
304
|
+
// src/rate-limiter.ts
|
|
305
|
+
var RateLimiter = class {
|
|
306
|
+
intervalMs;
|
|
307
|
+
maxTokens;
|
|
308
|
+
tokens;
|
|
309
|
+
lastRefill;
|
|
310
|
+
waitQueue = [];
|
|
311
|
+
constructor(options) {
|
|
312
|
+
if (options.requestsPerSecond <= 0) {
|
|
313
|
+
throw new Error("requestsPerSecond must be positive.");
|
|
314
|
+
}
|
|
315
|
+
if (options.burstLimit <= 0) {
|
|
316
|
+
throw new Error("burstLimit must be positive.");
|
|
317
|
+
}
|
|
318
|
+
this.intervalMs = 1e3 / options.requestsPerSecond;
|
|
319
|
+
this.maxTokens = options.burstLimit;
|
|
320
|
+
this.tokens = options.burstLimit;
|
|
321
|
+
this.lastRefill = Date.now();
|
|
322
|
+
}
|
|
323
|
+
async acquire() {
|
|
324
|
+
this.refill();
|
|
325
|
+
if (this.tokens >= 1) {
|
|
326
|
+
this.tokens -= 1;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const waitMs = this.intervalMs;
|
|
330
|
+
await new Promise((resolve) => {
|
|
331
|
+
const entry = { resolve };
|
|
332
|
+
this.waitQueue.push(entry);
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
const index = this.waitQueue.indexOf(entry);
|
|
335
|
+
if (index !== -1) {
|
|
336
|
+
this.waitQueue.splice(index, 1);
|
|
337
|
+
}
|
|
338
|
+
this.refill();
|
|
339
|
+
this.tokens = Math.max(0, this.tokens - 1);
|
|
340
|
+
resolve();
|
|
341
|
+
}, waitMs);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
refill() {
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
const elapsed = now - this.lastRefill;
|
|
347
|
+
const tokensToAdd = elapsed / this.intervalMs;
|
|
348
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
|
|
349
|
+
this.lastRefill = now;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
222
353
|
// src/runtime.ts
|
|
223
354
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
224
355
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
@@ -436,6 +567,8 @@ var ToolkitServer = class {
|
|
|
436
567
|
if (!inputSchema || !outputSchema) {
|
|
437
568
|
throw new ValidationError(`Tool '${definition.name}' requires both input and output schemas.`);
|
|
438
569
|
}
|
|
570
|
+
const timeoutMs = definition.timeoutMs ?? 3e4;
|
|
571
|
+
const isStreaming = definition.experimental_streamingResponse ?? false;
|
|
439
572
|
const storedTool = {
|
|
440
573
|
name: definition.name,
|
|
441
574
|
invoke: async (input, sessionId) => {
|
|
@@ -462,7 +595,34 @@ var ToolkitServer = class {
|
|
|
462
595
|
if (sessionId !== void 0) {
|
|
463
596
|
context.sessionId = sessionId;
|
|
464
597
|
}
|
|
465
|
-
const
|
|
598
|
+
const controller = new AbortController();
|
|
599
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
600
|
+
const timer = setTimeout(() => {
|
|
601
|
+
reject(new ToolTimeoutError(definition.name, timeoutMs));
|
|
602
|
+
}, timeoutMs);
|
|
603
|
+
controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
|
|
604
|
+
});
|
|
605
|
+
const handlerResult = await Promise.race([
|
|
606
|
+
definition.handler(parsedInputResult.data, context),
|
|
607
|
+
timeoutPromise
|
|
608
|
+
]);
|
|
609
|
+
controller.abort();
|
|
610
|
+
if (isStreaming && handlerResult !== null && typeof handlerResult === "object" && Symbol.asyncIterator in handlerResult) {
|
|
611
|
+
const chunks = [];
|
|
612
|
+
for await (const chunk of handlerResult) {
|
|
613
|
+
chunks.push(chunk);
|
|
614
|
+
}
|
|
615
|
+
const combined = chunks.join("");
|
|
616
|
+
const parsedOutputResult2 = await safeParseAsync(outputSchema, { text: combined });
|
|
617
|
+
if (!parsedOutputResult2.success) {
|
|
618
|
+
throw new ValidationError(
|
|
619
|
+
`Output validation failed for tool '${definition.name}': ${getParseErrorMessage(parsedOutputResult2.error)}`,
|
|
620
|
+
parsedOutputResult2.error
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
return parsedOutputResult2.data;
|
|
624
|
+
}
|
|
625
|
+
const output = handlerResult;
|
|
466
626
|
const parsedOutputResult = await safeParseAsync(outputSchema, output);
|
|
467
627
|
if (!parsedOutputResult.success) {
|
|
468
628
|
throw new ValidationError(
|
|
@@ -571,15 +731,23 @@ var ToolkitServer = class {
|
|
|
571
731
|
function defineTool(definition) {
|
|
572
732
|
return definition;
|
|
573
733
|
}
|
|
734
|
+
|
|
735
|
+
// src/index.ts
|
|
736
|
+
function createStreamingResponse(iterable) {
|
|
737
|
+
return iterable;
|
|
738
|
+
}
|
|
574
739
|
export {
|
|
575
740
|
ConfigurationError,
|
|
576
741
|
ExternalServiceError,
|
|
577
742
|
HttpServiceClient,
|
|
743
|
+
RateLimiter,
|
|
744
|
+
ToolTimeoutError,
|
|
578
745
|
ToolkitError,
|
|
579
746
|
ToolkitServer,
|
|
580
747
|
ValidationError,
|
|
581
748
|
createLogger,
|
|
582
749
|
createServerCard,
|
|
750
|
+
createStreamingResponse,
|
|
583
751
|
defineTool,
|
|
584
752
|
loadEnv,
|
|
585
753
|
normalizeError,
|
package/package.json
CHANGED