@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 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
- handler: (input: InferShape<TInputShape>, context: ToolkitToolExecutionContext) => Promise<InferShape<TOutputShape>>;
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
- 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 };
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
- const response = await fetch(url, requestInit);
184
- if (!response.ok) {
185
- const text = await response.text();
186
- this.logger.error(
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: response.status,
257
+ status: lastResponse.status,
190
258
  url: url.toString(),
191
- body: text
259
+ attempt: attempt + 1,
260
+ maxRetries: this.retryOptions.maxRetries,
261
+ delayMs: Math.round(delay)
192
262
  },
193
- "Upstream request failed"
263
+ "Retrying failed upstream request"
194
264
  );
195
- throw new ExternalServiceError(`${this.serviceName} request failed with status ${response.status}.`, {
196
- statusCode: response.status,
197
- details: text
198
- });
265
+ await lastResponse.text().catch(() => void 0);
266
+ await sleep(delay);
199
267
  }
200
- return response;
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 output = await definition.handler(parsedInputResult.data, context);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@universal-mcp-toolkit/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Shared runtime, validation, logging, transport, and DX primitives for universal-mcp-toolkit.",
6
6
  "license": "MIT",