toolcraft 0.0.409 → 0.0.411

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/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.88",
101
+ "version": "0.1.89",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.409",
116
+ "version": "0.0.411",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.409",
126
+ "version": "0.0.411",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.88",
101
+ "version": "0.1.89",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.409",
116
+ "version": "0.0.411",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.409",
126
+ "version": "0.0.411",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -389,6 +389,57 @@ CLI equivalent:
389
389
  npx tiny-http-mcp-server --stateless
390
390
  ```
391
391
 
392
+ ## Cloudflare Workers and Fetch hosts
393
+
394
+ Use the additive `tiny-http-mcp-server/fetch` entry point for a stateless
395
+ `Request` → `Response` handler. It shares the typed tool registry and protocol
396
+ engine with the Node server, without importing Node HTTP, stdio, or OAuth
397
+ modules. Existing `createHttpServer` integrations are unchanged.
398
+
399
+ ```ts
400
+ import { createFetchServer, defineSchema } from "tiny-http-mcp-server/fetch";
401
+
402
+ const mcp = createFetchServer<{ userId: string }>({
403
+ name: "images",
404
+ version: "1.0.0",
405
+ maxRequestBytes: 128 * 1024,
406
+ maxResponseBytes: 512 * 1024,
407
+ maxConcurrentToolCalls: 4,
408
+ maxQueuedToolCalls: 0
409
+ }).tool("whoami", "Identify the caller", defineSchema({}), (_, { context }) =>
410
+ context.userId
411
+ );
412
+
413
+ export default {
414
+ async fetch(request: Request, env: Env) {
415
+ // Verify the key with your existing authentication system on EVERY request.
416
+ const user = await authenticateApiKey(request, env);
417
+ if (!user) return new Response("Unauthorized", { status: 401 });
418
+ return mcp.fetch(request, { userId: user.id });
419
+ }
420
+ };
421
+ ```
422
+
423
+ Authentication and URL routing belong to your host. The second `fetch` argument
424
+ is trusted local state; JSON-RPC parameters and `_meta` cannot override it.
425
+ Tool handlers also receive `request` and a cancellation `signal`. Use that signal
426
+ for downstream work; aborted requests must not keep consuming resources.
427
+
428
+ The Fetch transport returns JSON for POST, acknowledges notifications with 202,
429
+ and returns 405 with `Allow: POST` for GET/DELETE and other methods. It creates no
430
+ sessions and rejects batch requests before executing any member. Clients must
431
+ send `Content-Type: application/json` and accept both `application/json` and
432
+ `text/event-stream`. Origins are rejected unless explicitly listed in
433
+ `allowedOrigins`; the host owns CORS preflight/response headers. Thrown handler
434
+ exceptions are sanitized; return an explicit MCP `isError` result for an error
435
+ message intended for callers.
436
+
437
+ `.tool(...)` and `.registerTool(...)` support the same typed schemas, structured
438
+ outputs, and content results as the Node API. Request and response byte limits
439
+ default to 1 MiB. Tool concurrency, queue bounds, active request limits, and tool
440
+ timeouts use the shared server options. Stateless Fetch does not support
441
+ subscriptions or server-initiated notification streams.
442
+
392
443
  ## API Reference
393
444
 
394
445
  The package re-exports the base server helpers from `tiny-stdio-mcp-server`, so you can import `defineSchema`, `createServer`, `Image`, `Audio`, `File`, and related types from here as well.
@@ -18,13 +18,23 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.88",
21
+ "version": "0.1.89",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
25
25
  "name": "tiny-mcp-client",
26
26
  "version": "0.1.0",
27
27
  "license": "MIT"
28
+ },
29
+ {
30
+ "name": "tiny-stdio-mcp-server",
31
+ "version": "0.1.27",
32
+ "license": "MIT"
33
+ },
34
+ {
35
+ "name": "toolcraft-schema",
36
+ "version": "0.0.1",
37
+ "license": "MIT"
28
38
  }
29
39
  ]
30
40
  }
@@ -0,0 +1,6 @@
1
+ export declare class FetchBodyError extends Error {
2
+ readonly status: 400 | 413;
3
+ constructor(status: 400 | 413, message: string);
4
+ }
5
+ /** Bound bytes while reading, including bodies without Content-Length. */
6
+ export declare function readFetchBody(request: Request, maxBytes: number): Promise<string>;
@@ -0,0 +1,47 @@
1
+ export class FetchBodyError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ }
7
+ }
8
+ /** Bound bytes while reading, including bodies without Content-Length. */
9
+ export async function readFetchBody(request, maxBytes) {
10
+ const length = request.headers.get("content-length");
11
+ if (length !== null && Number(length) > maxBytes) {
12
+ await request.body?.cancel();
13
+ throw new FetchBodyError(413, "Payload too large");
14
+ }
15
+ if (request.body === null)
16
+ return "";
17
+ const reader = request.body.getReader();
18
+ const decoder = new TextDecoder("utf-8", { fatal: true });
19
+ const abort = () => { void reader.cancel().catch(() => undefined); };
20
+ request.signal.addEventListener("abort", abort, { once: true });
21
+ let bytes = 0;
22
+ let text = "";
23
+ try {
24
+ request.signal.throwIfAborted();
25
+ while (true) {
26
+ const chunk = await reader.read();
27
+ request.signal.throwIfAborted();
28
+ if (chunk.done)
29
+ break;
30
+ bytes += chunk.value.byteLength;
31
+ if (bytes > maxBytes)
32
+ throw new FetchBodyError(413, "Payload too large");
33
+ text += decoder.decode(chunk.value, { stream: true });
34
+ }
35
+ return text + decoder.decode();
36
+ }
37
+ catch (error) {
38
+ await reader.cancel().catch(() => undefined);
39
+ if (error instanceof FetchBodyError || request.signal.aborted)
40
+ throw error;
41
+ throw new FetchBodyError(400, "Invalid request body");
42
+ }
43
+ finally {
44
+ request.signal.removeEventListener("abort", abort);
45
+ reader.releaseLock();
46
+ }
47
+ }
@@ -0,0 +1,23 @@
1
+ import { type CallToolResult, type HandlerRequestContext, type InputRequiredResult, type ServerOptions, type ToolDefinition, type ToolReturn, type TypedSchema, type TypedOutputSchema } from "tiny-stdio-mcp-server/core";
2
+ export { defineSchema, ToolError } from "tiny-stdio-mcp-server/core";
3
+ export type { CallToolResult, TypedSchema, TypedOutputSchema } from "tiny-stdio-mcp-server/core";
4
+ export interface FetchServerOptions extends ServerOptions {
5
+ /** Defaults to 1 MiB. Enforced while reading the stream. */
6
+ maxRequestBytes?: number;
7
+ /** Defaults to 1 MiB. */
8
+ maxResponseBytes?: number;
9
+ /** Browser origins allowed to call this endpoint; absent rejects all Origin headers. */
10
+ allowedOrigins?: readonly string[];
11
+ }
12
+ export interface FetchToolContext<TContext> extends HandlerRequestContext {
13
+ readonly request: Request;
14
+ readonly context: TContext;
15
+ }
16
+ export type FetchToolHandler<TIn, TContext, TOut = ToolReturn> = (input: TIn, context: FetchToolContext<TContext>) => TOut | CallToolResult | InputRequiredResult | Promise<TOut | CallToolResult | InputRequiredResult>;
17
+ export interface FetchServer<TContext = undefined> {
18
+ tool<TIn, TOut = never>(name: string, description: string, schema: TypedSchema<TIn>, handler: FetchToolHandler<TIn, TContext, TOut>, outputSchema?: TypedOutputSchema<TOut>): FetchServer<TContext>;
19
+ registerTool<TIn, TOut = never>(definition: Omit<ToolDefinition<TIn, TOut>, "handler">, handler: FetchToolHandler<TIn, TContext, TOut>): FetchServer<TContext>;
20
+ fetch(request: Request, ...context: undefined extends TContext ? [context?: TContext] : [context: TContext]): Promise<Response>;
21
+ }
22
+ /** Stateless Streamable HTTP. Authentication must run in the host before fetch(). */
23
+ export declare function createFetchServer<TContext = undefined>(options: FetchServerOptions): FetchServer<TContext>;
@@ -0,0 +1,130 @@
1
+ import { createProtocolServer, ToolError } from "tiny-stdio-mcp-server/core";
2
+ import { parseMessage } from "tiny-stdio-mcp-server/jsonrpc";
3
+ import { MODERN_PROTOCOL_VERSION } from "tiny-stdio-mcp-server/protocol";
4
+ import { FetchBodyError, readFetchBody } from "./fetch-body.js";
5
+ import { validateModernHeaders } from "./modern-headers.js";
6
+ export { defineSchema, ToolError } from "tiny-stdio-mcp-server/core";
7
+ const versions = new Set(["2025-03-26", "2025-06-18", "2025-11-25", MODERN_PROTOCOL_VERSION]);
8
+ const unsupportedMethods = new Set(["resources/subscribe", "resources/unsubscribe", "subscriptions/listen"]);
9
+ function json(value, status = 200) {
10
+ return Response.json(value, { status, headers: { "cache-control": "no-store", "x-content-type-options": "nosniff" } });
11
+ }
12
+ function failure(code, message, status = 400, id = null) {
13
+ return json({ jsonrpc: "2.0", id, error: { code, message } }, status);
14
+ }
15
+ function validateRequest(request, options) {
16
+ const origin = request.headers.get("origin");
17
+ if (origin !== null && !options.allowedOrigins?.includes(origin))
18
+ return failure(-32600, "Origin not allowed", 403);
19
+ if (request.method !== "POST") {
20
+ const response = failure(-32600, "Method not allowed", 405);
21
+ response.headers.set("allow", "POST");
22
+ return response;
23
+ }
24
+ if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json")
25
+ return failure(-32600, "Content-Type must be application/json", 415);
26
+ const accepted = request.headers.get("accept")?.split(",").map(value => value.split(";")[0]?.trim().toLowerCase());
27
+ if (!accepted?.includes("application/json") || !accepted.includes("text/event-stream"))
28
+ return failure(-32600, "Accept must include application/json and text/event-stream", 406);
29
+ const version = request.headers.get("mcp-protocol-version");
30
+ if (version !== null && !versions.has(version))
31
+ return failure(-32600, "Unsupported protocol version");
32
+ if (request.headers.has("mcp-session-id"))
33
+ return failure(-32600, "This endpoint is stateless", 400);
34
+ }
35
+ /** Stateless Streamable HTTP. Authentication must run in the host before fetch(). */
36
+ export function createFetchServer(options) {
37
+ const maxRequestBytes = options.maxRequestBytes ?? 1024 * 1024;
38
+ const maxResponseBytes = options.maxResponseBytes ?? 1024 * 1024;
39
+ for (const bound of [maxRequestBytes, maxResponseBytes]) {
40
+ if (!Number.isSafeInteger(bound) || bound < 1)
41
+ throw new Error("Fetch byte limits must be positive safe integers");
42
+ }
43
+ const protocol = createProtocolServer({ ...options, supportNotifications: false, supportResourceSubscriptions: false });
44
+ function wrap(handler) {
45
+ return async (input, context) => {
46
+ const local = context.localContext;
47
+ try {
48
+ return await handler(input, { ...context, ...local });
49
+ }
50
+ catch {
51
+ // Tool-authored error results are public; thrown exceptions may contain credentials.
52
+ throw new ToolError(-32603, "Tool execution failed");
53
+ }
54
+ };
55
+ }
56
+ const server = {
57
+ tool(name, description, schema, handler, outputSchema) {
58
+ protocol.tool(name, description, schema, wrap(handler), outputSchema);
59
+ return server;
60
+ },
61
+ registerTool(definition, handler) {
62
+ protocol.registerTool(definition, wrap(handler));
63
+ return server;
64
+ },
65
+ async fetch(request, ...[context]) {
66
+ const invalid = validateRequest(request, options);
67
+ if (invalid !== undefined)
68
+ return invalid;
69
+ let raw;
70
+ try {
71
+ raw = await readFetchBody(request, maxRequestBytes);
72
+ }
73
+ catch (error) {
74
+ if (request.signal.aborted)
75
+ throw error;
76
+ return failure(-32600, error instanceof FetchBodyError ? error.message : "Invalid request body", error instanceof FetchBodyError ? error.status : 400);
77
+ }
78
+ // MCP 2025-06-18 and later prohibit batches. Reject before any side effects.
79
+ const parsed = parseMessage(raw);
80
+ if (!parsed.success)
81
+ return failure(parsed.error.code, parsed.error.message, 400, parsed.id);
82
+ if (parsed.isNotification)
83
+ return new Response(null, { status: 202 });
84
+ const message = parsed.request;
85
+ if (!("id" in message))
86
+ return failure(-32600, "Invalid Request");
87
+ if (message.method.startsWith("notifications/"))
88
+ return failure(-32600, "Invalid Request", 400, message.id);
89
+ if (unsupportedMethods.has(message.method))
90
+ return failure(-32601, "Method not found", 200, message.id);
91
+ const headers = Object.fromEntries(request.headers);
92
+ const metadata = message.params?._meta;
93
+ const modern = request.headers.get("mcp-protocol-version") === MODERN_PROTOCOL_VERSION ||
94
+ metadata?.["io.modelcontextprotocol/protocolVersion"] === MODERN_PROTOCOL_VERSION;
95
+ if (modern) {
96
+ const error = validateModernHeaders(headers, message);
97
+ if (error !== undefined)
98
+ return json({ jsonrpc: "2.0", id: message.id, error }, 400);
99
+ }
100
+ // Fresh lifecycle per request isolates duplicate IDs and cancellation across callers.
101
+ const session = protocol.createMessageSession();
102
+ try {
103
+ if (!modern && message.method !== "initialize") {
104
+ await session.handleMessage("initialize", { protocolVersion: request.headers.get("mcp-protocol-version") ?? "2025-03-26" });
105
+ }
106
+ const handled = await session.handleMessage(message.method, message.params, {
107
+ ...(message.id !== null ? { requestId: message.id } : {}),
108
+ signal: request.signal,
109
+ parameterHeaders: headers,
110
+ localContext: { request, context }
111
+ });
112
+ request.signal.throwIfAborted();
113
+ const response = { jsonrpc: "2.0", id: message.id, ...handled };
114
+ const body = JSON.stringify(response);
115
+ if (new TextEncoder().encode(body).byteLength > maxResponseBytes)
116
+ return failure(-32603, "Response exceeds byte limit", 200, message.id);
117
+ return json(response);
118
+ }
119
+ catch (error) {
120
+ if (request.signal.aborted)
121
+ throw error;
122
+ return failure(-32603, "Internal error", 200, message.id);
123
+ }
124
+ finally {
125
+ session.close();
126
+ }
127
+ }
128
+ };
129
+ return server;
130
+ }
@@ -1,4 +1,3 @@
1
- import { JSON_RPC_ERROR_CODES } from "tiny-stdio-mcp-server";
2
1
  import { decodeHeaderValue } from "tiny-stdio-mcp-server/headers";
3
2
  export function validateModernHeaders(headers, request) {
4
3
  const metadata = request.params?._meta;
@@ -7,7 +6,7 @@ export function validateModernHeaders(headers, request) {
7
6
  : undefined;
8
7
  if (typeof version !== "string")
9
8
  return {
10
- code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
9
+ code: -32602,
11
10
  message: "Missing protocol version request metadata"
12
11
  };
13
12
  const expected = { "mcp-protocol-version": version };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,10 @@
24
24
  "./test-support": {
25
25
  "types": "./dist/test-support.d.ts",
26
26
  "import": "./dist/test-support.js"
27
+ },
28
+ "./fetch": {
29
+ "types": "./dist/fetch.d.ts",
30
+ "import": "./dist/fetch.js"
27
31
  }
28
32
  },
29
33
  "scripts": {
@@ -245,3 +245,14 @@ per-tool schemas, and per-call content helper options such as `maxBytes` for rem
245
245
  ## License
246
246
 
247
247
  MIT
248
+
249
+ ## Protocol-only hosts
250
+
251
+ Import `createProtocolServer` and `defineSchema` from
252
+ `tiny-stdio-mcp-server/core` when your host supplies its own transport. This
253
+ entry point loads no Node stdio modules. Use `createMessageSession()` for an
254
+ isolated request lifecycle and close it when the transport completes. The
255
+ `handleMessage` context accepts `localContext` for host-owned state; tool
256
+ handlers receive it separately from client parameters and metadata. Use the
257
+ ordinary root `createServer` export for `listen()` or `connect()`; the core
258
+ entry point does not provide a stdio runtime.
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.409",
11
+ "version": "0.0.411",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -0,0 +1,8 @@
1
+ /** Protocol engine and typed tools without Node transport imports. */
2
+ export { createProtocolServer } from "./protocol-server.js";
3
+ export type { Server, MessageRequestContext, MessageSession } from "./protocol-server.js";
4
+ export { defineSchema } from "./schema.js";
5
+ export type { TypedSchema, TypedOutputSchema } from "./schema.js";
6
+ export { ToolError } from "./types.js";
7
+ export type { CallToolResult, HandlerRequestContext, InputRequiredResult, ServerOptions, ToolDefinition } from "./types.js";
8
+ export type { ToolReturn } from "./content/convert.js";
@@ -0,0 +1,4 @@
1
+ /** Protocol engine and typed tools without Node transport imports. */
2
+ export { createProtocolServer } from "./protocol-server.js";
3
+ export { defineSchema } from "./schema.js";
4
+ export { ToolError } from "./types.js";
@@ -0,0 +1,48 @@
1
+ import type { ServerOptions, ToolDefinition, ToolHandler, HandleResult, Prompt, PromptHandler, Resource, ResourceHandler, ResourceTemplate, Transport, SDKTransport, JSONRPCNotification } from "./types.js";
2
+ import type { TypedSchema, TypedOutputSchema } from "./schema.js";
3
+ interface StdioRuntime {
4
+ readline: typeof import("node:readline");
5
+ StdioInput: typeof import("./stdio-input.js").StdioInput;
6
+ StdioOutput: typeof import("./stdio-output.js").StdioOutput;
7
+ }
8
+ export interface Server {
9
+ tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: ToolHandler<TIn, TOut>, outputSchema?: TypedOutputSchema<TOut>): Server;
10
+ registerTool<TIn, TOut = never>(definition: Omit<ToolDefinition<TIn, TOut>, "handler">, handler: ToolHandler<TIn, TOut>): Server;
11
+ prompt(definition: Prompt, handler: PromptHandler): Server;
12
+ resource(definition: Resource, handler: ResourceHandler): Server;
13
+ resourceTemplate(definition: ResourceTemplate, handler: ResourceHandler): Server;
14
+ method(name: string, handler: CustomMethodHandler): Server;
15
+ onNotification(listener: (notification: JSONRPCNotification) => void): () => void;
16
+ removeTool(name: string): boolean;
17
+ removePrompt(name: string): boolean;
18
+ removeResource(uri: string): boolean;
19
+ removeResourceTemplate(uriTemplate: string): boolean;
20
+ notifyToolsChanged(): Promise<void>;
21
+ notifyPromptsChanged(): Promise<void>;
22
+ notifyResourcesChanged(): Promise<void>;
23
+ notifyResourceUpdated(uri: string): Promise<void>;
24
+ createMessageSession(listener?: (notification: JSONRPCNotification) => void | Promise<void>): MessageSession;
25
+ handleMessage(method: string, params?: Record<string, unknown>, context?: MessageRequestContext): Promise<HandleResult>;
26
+ listen(): Promise<void>;
27
+ connect(transport: Transport): Promise<void>;
28
+ connectSDK(transport: SDKTransport): Promise<void>;
29
+ }
30
+ export interface MessageSessionContext {
31
+ readonly signal: AbortSignal;
32
+ notify(method: string, params?: Record<string, unknown>): Promise<void>;
33
+ }
34
+ export interface MessageRequestContext {
35
+ requestId?: string | number;
36
+ signal?: AbortSignal;
37
+ parameterHeaders?: Record<string, string | string[] | undefined>;
38
+ /** Host-only state, never read from JSON-RPC params or metadata. */
39
+ localContext?: unknown;
40
+ }
41
+ export type CustomMethodHandler = (params: Record<string, unknown> | undefined, session: MessageSessionContext) => unknown | Promise<unknown>;
42
+ export type MessageHandler = (method: string, params?: Record<string, unknown>, context?: MessageRequestContext) => Promise<HandleResult>;
43
+ export interface MessageSession {
44
+ handleMessage: MessageHandler;
45
+ close(): void;
46
+ }
47
+ export declare function createProtocolServer(options: ServerOptions, stdio?: StdioRuntime): Server;
48
+ export {};