@buildinternet/uploads 0.35.1 → 0.36.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.
@@ -1,7 +1,8 @@
1
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
2
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
1
3
  import { parseCommandArgs } from "../cli-args.js";
2
4
  import { resolveApiUrl } from "../config.js";
3
5
  import { createMcpServer } from "../mcp/server.js";
4
- import { serveStdio } from "../mcp/stdio.js";
5
6
  import { createUploadsMcpTools } from "../mcp/tools.js";
6
7
  import { packageVersion } from "../package-version.js";
7
8
  import { writeCommandHelp } from "../cli-style.js";
@@ -29,11 +30,23 @@ export async function runMcp(args, opts, help = false) {
29
30
  writeCommandHelp(MCP_HELP);
30
31
  return 0;
31
32
  }
32
- const server = createMcpServer({
33
+ // Ajv is the right provider here and only here: it compiles schemas at
34
+ // runtime, which Node allows and workerd does not (apps/mcp passes the
35
+ // `@cfworker/json-schema` provider instead).
36
+ const validator = new AjvJsonSchemaValidator();
37
+ const handle = serveStdio(() => createMcpServer({
33
38
  serverInfo: { name: "uploads", version: packageVersion() },
34
39
  tools: createUploadsMcpTools({ globals: opts.globals }),
35
40
  apiUrl: resolveApiUrl(opts.globals),
41
+ validator,
42
+ }));
43
+ // `serveStdio` hands back only a teardown handle, so the command owns the
44
+ // wait: serving ends when the client closes our stdin, which is what the
45
+ // previous readline loop returned on.
46
+ await new Promise((resolve) => {
47
+ process.stdin.once("end", resolve);
48
+ process.stdin.once("close", resolve);
36
49
  });
37
- await serveStdio(server);
50
+ await handle.close();
38
51
  return 0;
39
52
  }
@@ -1,6 +1,27 @@
1
+ /**
2
+ * MCP (Model Context Protocol) server core, built on the v2 TypeScript SDK
3
+ * (`@modelcontextprotocol/server`), which speaks spec `2026-07-28` and the
4
+ * 2025-era revisions side by side.
5
+ *
6
+ * Tools stay declarative: callers pass `McpTool[]` with hand-written JSON
7
+ * Schema and this module registers them with the SDK. That keeps the two tool
8
+ * sets (./tools.ts and apps/mcp/src/tools.ts) free of SDK imports and confines
9
+ * the dependency to this file.
10
+ *
11
+ * Runtime-agnostic, so the JSON Schema validator is injected rather than
12
+ * chosen here: the SDK bundles no provider in its root entry, and the two
13
+ * available ones are not interchangeable — Ajv compiles schemas at runtime and
14
+ * workerd rejects that, so Workers callers must pass the `@cfworker/json-schema`
15
+ * provider. See `@modelcontextprotocol/server/validators/{ajv,cf-worker}`.
16
+ *
17
+ * The stdio transport comes from `@modelcontextprotocol/server/stdio`; logs
18
+ * must never go to stdout.
19
+ */
20
+ import { McpServer, type jsonSchemaValidator } from "@modelcontextprotocol/server";
1
21
  export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCanonical, metadataProp, stateProp, optBool, optPosInt, optString, optStringArray, optStringRecord, usage, type ToolArgs, } from "./args.js";
2
22
  export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
3
23
  export { mapBounded } from "../async.js";
24
+ export { McpServer, type jsonSchemaValidator };
4
25
  export interface McpTool {
5
26
  name: string;
6
27
  description: string;
@@ -8,10 +29,6 @@ export interface McpTool {
8
29
  inputSchema: Record<string, unknown>;
9
30
  handler: (args: Record<string, unknown>) => Promise<unknown>;
10
31
  }
11
- export interface McpServer {
12
- /** Handle one JSON-RPC line. Undefined for notifications / client responses. */
13
- handleLine(line: string): Promise<string | undefined>;
14
- }
15
32
  export declare function createMcpServer(opts: {
16
33
  serverInfo: {
17
34
  name: string;
@@ -20,4 +37,9 @@ export declare function createMcpServer(opts: {
20
37
  tools: McpTool[];
21
38
  /** API base for telemetry (honors uploads --api-url). */
22
39
  apiUrl?: string;
40
+ /**
41
+ * Runtime's JSON Schema validator. Node callers pass the Ajv provider;
42
+ * Workers callers must pass the `@cfworker/json-schema` one.
43
+ */
44
+ validator: jsonSchemaValidator;
23
45
  }): McpServer;
@@ -1,58 +1,62 @@
1
1
  /**
2
- * Minimal, dependency-free MCP (Model Context Protocol) server core.
2
+ * MCP (Model Context Protocol) server core, built on the v2 TypeScript SDK
3
+ * (`@modelcontextprotocol/server`), which speaks spec `2026-07-28` and the
4
+ * 2025-era revisions side by side.
3
5
  *
4
- * Transport is one JSON-RPC 2.0 message per line/request. This module is
5
- * transport- and runtime-agnostic (usable from Workers as well as Node)
6
- * `handleLine` takes a raw message string and returns the serialized response
7
- * (or undefined when no response is due), so it is directly testable. The
8
- * stdio transport lives in ./stdio.ts; logs must never go to stdout.
6
+ * Tools stay declarative: callers pass `McpTool[]` with hand-written JSON
7
+ * Schema and this module registers them with the SDK. That keeps the two tool
8
+ * sets (./tools.ts and apps/mcp/src/tools.ts) free of SDK imports and confines
9
+ * the dependency to this file.
10
+ *
11
+ * Runtime-agnostic, so the JSON Schema validator is injected rather than
12
+ * chosen here: the SDK bundles no provider in its root entry, and the two
13
+ * available ones are not interchangeable — Ajv compiles schemas at runtime and
14
+ * workerd rejects that, so Workers callers must pass the `@cfworker/json-schema`
15
+ * provider. See `@modelcontextprotocol/server/validators/{ajv,cf-worker}`.
16
+ *
17
+ * The stdio transport comes from `@modelcontextprotocol/server/stdio`; logs
18
+ * must never go to stdout.
9
19
  */
20
+ import { fromJsonSchema, McpServer, } from "@modelcontextprotocol/server";
10
21
  import { UploadsError } from "../errors.js";
11
22
  import { errorCodeFromUnknown, recordEvent } from "../telemetry.js";
12
23
  import { ToolBatchError } from "./batch-error.js";
13
24
  export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCanonical, metadataProp, stateProp, optBool, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
14
25
  export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
15
26
  export { mapBounded } from "../async.js";
16
- const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-06-18", "2025-03-26", "2024-11-05"]);
17
- const LATEST_PROTOCOL_VERSION = "2025-06-18";
18
- function response(id, result) {
19
- return JSON.stringify({ jsonrpc: "2.0", id, result });
20
- }
21
- function errorResponse(id, code, message) {
22
- return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
23
- }
27
+ export { McpServer };
28
+ /**
29
+ * The tool catalog is fixed for the lifetime of a deploy, so a generous
30
+ * freshness hint is honest. `private` rather than `public` because the list is
31
+ * behind auth and, on the hosted worker, filtered by the caller's token
32
+ * scopes a shared intermediary must never serve one caller's tool list to
33
+ * another.
34
+ */
35
+ const TOOLS_LIST_CACHE_HINT = { ttlMs: 3_600_000, cacheScope: "private" };
24
36
  /** Tool failures become tool results (isError), never JSON-RPC errors. */
25
37
  function toolErrorText(err) {
26
38
  if (err instanceof UploadsError)
27
39
  return `${err.message} (${err.code})`;
28
40
  return err instanceof Error ? err.message : String(err);
29
41
  }
30
- export function createMcpServer(opts) {
31
- const { serverInfo, tools, apiUrl } = opts;
32
- async function callTool(id, params) {
33
- const name = params.name;
34
- const tool = typeof name === "string" ? tools.find((t) => t.name === name) : undefined;
35
- if (!tool)
36
- return errorResponse(id, -32602, `unknown tool: ${String(name ?? "(missing)")}`);
37
- const args = params.arguments ?? {};
38
- if (typeof args !== "object" || args === null || Array.isArray(args)) {
39
- return errorResponse(id, -32602, "tool arguments must be an object");
40
- }
42
+ /**
43
+ * Wraps a tool handler so its outcome becomes a `CallToolResult` and every
44
+ * call is recorded. A throw is reported to the client as an errored tool
45
+ * result rather than a protocol error, which is what lets an agent read the
46
+ * message and retry.
47
+ */
48
+ function wrapHandler(tool, apiUrl) {
49
+ const command = `tool ${tool.name}`.slice(0, 120);
50
+ return async (args) => {
41
51
  const start = Date.now();
42
- const command = `tool ${tool.name}`.slice(0, 120);
43
52
  try {
44
- const result = await tool.handler(args);
45
- recordEvent({
46
- surface: "mcp",
47
- command,
48
- exitCode: 0,
49
- durationMs: Date.now() - start,
50
- }, { apiUrl });
51
- return response(id, {
53
+ const result = await tool.handler(args ?? {});
54
+ recordEvent({ surface: "mcp", command, exitCode: 0, durationMs: Date.now() - start }, { apiUrl });
55
+ return {
52
56
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
53
57
  structuredContent: result,
54
58
  isError: false,
55
- });
59
+ };
56
60
  }
57
61
  catch (err) {
58
62
  recordEvent({
@@ -65,82 +69,26 @@ export function createMcpServer(opts) {
65
69
  // Multi-file total failure: keep structuredContent so agents see every
66
70
  // per-file error, not only the first message string.
67
71
  if (err instanceof ToolBatchError) {
68
- return response(id, {
69
- content: [
70
- {
71
- type: "text",
72
- text: JSON.stringify(err.structuredContent, null, 2),
73
- },
74
- ],
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify(err.structuredContent, null, 2) }],
75
74
  structuredContent: err.structuredContent,
76
75
  isError: true,
77
- });
76
+ };
78
77
  }
79
- return response(id, {
80
- content: [{ type: "text", text: toolErrorText(err) }],
81
- isError: true,
82
- });
78
+ return { content: [{ type: "text", text: toolErrorText(err) }], isError: true };
83
79
  }
84
- }
85
- return {
86
- async handleLine(line) {
87
- let msg;
88
- try {
89
- msg = JSON.parse(line);
90
- }
91
- catch {
92
- return errorResponse(null, -32700, "Parse error");
93
- }
94
- // JSON-RPC batching was removed from MCP: arrays are invalid requests.
95
- if (typeof msg !== "object" || msg === null || Array.isArray(msg)) {
96
- return errorResponse(null, -32600, "Invalid Request");
97
- }
98
- const record = msg;
99
- const { method, params } = record;
100
- // A response from the client (has result/error, no method): ignore.
101
- if (method === undefined && ("result" in record || "error" in record))
102
- return undefined;
103
- const id = typeof record.id === "string" || typeof record.id === "number" ? record.id : null;
104
- if (typeof method !== "string")
105
- return errorResponse(id, -32600, "Invalid Request");
106
- if (method.startsWith("notifications/"))
107
- return undefined;
108
- // A request without an id is a notification — never respond.
109
- if (!("id" in record))
110
- return undefined;
111
- const p = (typeof params === "object" && params !== null && !Array.isArray(params) ? params : {});
112
- try {
113
- switch (method) {
114
- case "initialize": {
115
- const requested = typeof p.protocolVersion === "string" ? p.protocolVersion : "";
116
- const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requested)
117
- ? requested
118
- : LATEST_PROTOCOL_VERSION;
119
- return response(id, {
120
- protocolVersion,
121
- capabilities: { tools: {} },
122
- serverInfo,
123
- });
124
- }
125
- case "ping":
126
- return response(id, {});
127
- case "tools/list":
128
- return response(id, {
129
- tools: tools.map(({ name, description, inputSchema }) => ({
130
- name,
131
- description,
132
- inputSchema,
133
- })),
134
- });
135
- case "tools/call":
136
- return await callTool(id, p);
137
- default:
138
- return errorResponse(id, -32601, `method not found: ${method}`);
139
- }
140
- }
141
- catch (err) {
142
- return errorResponse(id, -32603, err instanceof Error ? err.message : String(err));
143
- }
144
- },
145
80
  };
146
81
  }
82
+ export function createMcpServer(opts) {
83
+ const { serverInfo, tools, apiUrl, validator } = opts;
84
+ const server = new McpServer(serverInfo, {
85
+ cacheHints: { "tools/list": TOOLS_LIST_CACHE_HINT },
86
+ });
87
+ for (const tool of tools) {
88
+ server.registerTool(tool.name, {
89
+ description: tool.description,
90
+ inputSchema: fromJsonSchema(tool.inputSchema, validator),
91
+ }, wrapHandler(tool, apiUrl));
92
+ }
93
+ return server;
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.35.1",
3
+ "version": "0.36.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -57,6 +57,7 @@
57
57
  "provenance": true
58
58
  },
59
59
  "dependencies": {
60
+ "@modelcontextprotocol/server": "^2.0.0",
60
61
  "exif-reader": "^2.0.3",
61
62
  "opentype.js": "^2.0.0",
62
63
  "perfect-freehand": "^1.2.3",
@@ -1,3 +0,0 @@
1
- import type { McpServer } from "./server.js";
2
- /** Serve the MCP protocol on stdin/stdout; resolves when stdin ends. */
3
- export declare function serveStdio(server: McpServer): Promise<void>;
package/dist/mcp/stdio.js DELETED
@@ -1,14 +0,0 @@
1
- /** Stdio transport for the MCP server core (Node-only; the core itself is runtime-agnostic). */
2
- import { createInterface } from "node:readline";
3
- import { writeStdout } from "../io.js";
4
- /** Serve the MCP protocol on stdin/stdout; resolves when stdin ends. */
5
- export async function serveStdio(server) {
6
- const rl = createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
7
- for await (const line of rl) {
8
- if (!line.trim())
9
- continue;
10
- const out = await server.handleLine(line);
11
- if (out !== undefined)
12
- await writeStdout(out + "\n");
13
- }
14
- }