tiny-stdio-mcp-server 0.1.1 → 0.1.3

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,4 +1,4 @@
1
- import { fileTypeFromBuffer } from "./mime.js";
1
+ import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
2
2
  const SUPPORTED_AUDIO_MIMES = new Set([
3
3
  "audio/mpeg",
4
4
  "audio/wav",
@@ -22,7 +22,7 @@ export class Audio {
22
22
  static async fromUrl(url) {
23
23
  const response = await fetch(url);
24
24
  if (!response.ok) {
25
- throw new Error(`Failed to fetch audio from ${url}: ${response.status} ${response.statusText}`);
25
+ throw new Error(`Failed to fetch audio from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
26
26
  }
27
27
  const arrayBuffer = await response.arrayBuffer();
28
28
  const data = new Uint8Array(arrayBuffer);
@@ -32,12 +32,12 @@ export class Audio {
32
32
  mimeType = detected.mime;
33
33
  }
34
34
  else {
35
- const contentType = response.headers.get("content-type")?.split(";")[0];
35
+ const contentType = parseContentType(response.headers.get("content-type")).mimeType;
36
36
  if (contentType && SUPPORTED_AUDIO_MIMES.has(contentType)) {
37
37
  mimeType = contentType;
38
38
  }
39
39
  else {
40
- throw new Error(`Unable to detect audio MIME type from ${url}`);
40
+ throw new Error(`Unable to detect audio MIME type from ${safeRemoteLabel(url)}`);
41
41
  }
42
42
  }
43
43
  const base64 = Buffer.from(data).toString("base64");
@@ -47,11 +47,14 @@ export class Audio {
47
47
  let mimeType;
48
48
  if (format) {
49
49
  if (format.includes("/")) {
50
- mimeType = format;
50
+ mimeType = format.toLowerCase();
51
51
  }
52
52
  else {
53
53
  mimeType = AUDIO_FORMAT_MAP[format.toLowerCase()] || `audio/${format}`;
54
54
  }
55
+ if (!SUPPORTED_AUDIO_MIMES.has(mimeType)) {
56
+ throw new Error(`Unsupported audio MIME type: ${mimeType}`);
57
+ }
55
58
  }
56
59
  else {
57
60
  const detected = fileTypeFromBuffer(data);
@@ -64,7 +67,12 @@ export class Audio {
64
67
  return new Audio(base64, mimeType);
65
68
  }
66
69
  static fromBase64(base64, mimeType) {
67
- return new Audio(base64, mimeType);
70
+ assertBase64(base64);
71
+ const normalizedMimeType = mimeType.toLowerCase();
72
+ if (!SUPPORTED_AUDIO_MIMES.has(normalizedMimeType)) {
73
+ throw new Error(`Unsupported audio MIME type: ${normalizedMimeType}`);
74
+ }
75
+ return new Audio(base64, normalizedMimeType);
68
76
  }
69
77
  toContentBlock() {
70
78
  return {
@@ -17,6 +17,7 @@ export declare class File {
17
17
  private readonly mimeType;
18
18
  private readonly isText;
19
19
  private readonly name?;
20
+ private readonly charset;
20
21
  private constructor();
21
22
  static fromUrl(url: string): Promise<File>;
22
23
  static fromBytes(data: Uint8Array, mimeType: string): File;
@@ -1,26 +1,31 @@
1
- import { fileTypeFromBuffer } from "./mime.js";
1
+ import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
2
2
  function isTextMimeType(mimeType) {
3
- return (mimeType.startsWith("text/") ||
4
- mimeType === "application/json" ||
5
- mimeType === "application/xml" ||
6
- mimeType === "application/javascript" ||
7
- mimeType === "application/typescript");
3
+ const normalizedMimeType = mimeType.toLowerCase();
4
+ return (normalizedMimeType.startsWith("text/") ||
5
+ normalizedMimeType === "application/json" ||
6
+ normalizedMimeType.endsWith("+json") ||
7
+ normalizedMimeType === "application/xml" ||
8
+ normalizedMimeType.endsWith("+xml") ||
9
+ normalizedMimeType === "application/javascript" ||
10
+ normalizedMimeType === "application/typescript");
8
11
  }
9
12
  export class File {
10
13
  data;
11
14
  mimeType;
12
15
  isText;
13
16
  name;
14
- constructor(data, mimeType, isText, name) {
17
+ charset;
18
+ constructor(data, mimeType, isText, name, charset = "utf-8") {
15
19
  this.data = data;
16
20
  this.mimeType = mimeType;
17
21
  this.isText = isText;
18
22
  this.name = name;
23
+ this.charset = charset;
19
24
  }
20
25
  static async fromUrl(url) {
21
26
  const response = await fetch(url);
22
27
  if (!response.ok) {
23
- throw new Error(`Failed to fetch file from ${url}: ${response.status} ${response.statusText}`);
28
+ throw new Error(`Failed to fetch file from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
24
29
  }
25
30
  const arrayBuffer = await response.arrayBuffer();
26
31
  const data = new Uint8Array(arrayBuffer);
@@ -30,26 +35,37 @@ export class File {
30
35
  mimeType = detected.mime;
31
36
  }
32
37
  else {
33
- const contentType = response.headers.get("content-type")?.split(";")[0];
34
- if (contentType) {
35
- mimeType = contentType;
38
+ const contentType = parseContentType(response.headers.get("content-type"));
39
+ if (contentType.mimeType) {
40
+ mimeType = contentType.mimeType;
36
41
  }
37
42
  else {
38
- throw new Error(`Unable to detect MIME type from ${url}`);
43
+ throw new Error(`Unable to detect MIME type from ${safeRemoteLabel(url)}`);
39
44
  }
40
45
  }
41
- const isText = isTextMimeType(mimeType);
42
- const name = url.split("/").pop() || "file";
43
- return new File(data, mimeType, isText, name);
46
+ const contentType = parseContentType(response.headers.get("content-type"));
47
+ const charset = contentType.charset ?? "utf-8";
48
+ let isText = isTextMimeType(mimeType);
49
+ if (isText) {
50
+ try {
51
+ new TextDecoder(charset);
52
+ }
53
+ catch {
54
+ isText = false;
55
+ }
56
+ }
57
+ const name = new URL(url).pathname.split("/").pop() || "file";
58
+ return new File(data, mimeType, isText, name, charset);
44
59
  }
45
60
  static fromBytes(data, mimeType) {
46
61
  const isText = isTextMimeType(mimeType);
47
62
  return new File(data, mimeType, isText);
48
63
  }
49
64
  static fromText(text, mimeType = "text/plain") {
50
- return new File(text, mimeType, true);
65
+ return new File(text, mimeType, isTextMimeType(mimeType));
51
66
  }
52
67
  static fromBase64(base64, mimeType) {
68
+ assertBase64(base64);
53
69
  const data = Buffer.from(base64, "base64");
54
70
  const isText = isTextMimeType(mimeType);
55
71
  return new File(new Uint8Array(data), mimeType, isText);
@@ -62,7 +78,7 @@ export class File {
62
78
  text = this.data;
63
79
  }
64
80
  else {
65
- text = new TextDecoder("utf-8").decode(this.data);
81
+ text = new TextDecoder(this.charset).decode(this.data);
66
82
  }
67
83
  return {
68
84
  type: "resource",
@@ -1,4 +1,4 @@
1
- import { fileTypeFromBuffer } from "./mime.js";
1
+ import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
2
2
  const SUPPORTED_IMAGE_MIMES = new Set([
3
3
  "image/png",
4
4
  "image/jpeg",
@@ -15,7 +15,7 @@ export class Image {
15
15
  static async fromUrl(url) {
16
16
  const response = await fetch(url);
17
17
  if (!response.ok) {
18
- throw new Error(`Failed to fetch image from ${url}: ${response.status} ${response.statusText}`);
18
+ throw new Error(`Failed to fetch image from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
19
19
  }
20
20
  const arrayBuffer = await response.arrayBuffer();
21
21
  const data = new Uint8Array(arrayBuffer);
@@ -25,12 +25,12 @@ export class Image {
25
25
  mimeType = detected.mime;
26
26
  }
27
27
  else {
28
- const contentType = response.headers.get("content-type")?.split(";")[0];
28
+ const contentType = parseContentType(response.headers.get("content-type")).mimeType;
29
29
  if (contentType && SUPPORTED_IMAGE_MIMES.has(contentType)) {
30
30
  mimeType = contentType;
31
31
  }
32
32
  else {
33
- throw new Error(`Unable to detect image MIME type from ${url}`);
33
+ throw new Error(`Unable to detect image MIME type from ${safeRemoteLabel(url)}`);
34
34
  }
35
35
  }
36
36
  const base64 = Buffer.from(data).toString("base64");
@@ -39,7 +39,10 @@ export class Image {
39
39
  static fromBytes(data, format) {
40
40
  let mimeType;
41
41
  if (format) {
42
- mimeType = format.includes("/") ? format : `image/${format}`;
42
+ mimeType = (format.includes("/") ? format : `image/${format}`).toLowerCase();
43
+ if (!SUPPORTED_IMAGE_MIMES.has(mimeType)) {
44
+ throw new Error(`Unsupported image MIME type: ${mimeType}`);
45
+ }
43
46
  }
44
47
  else {
45
48
  const detected = fileTypeFromBuffer(data);
@@ -52,7 +55,12 @@ export class Image {
52
55
  return new Image(base64, mimeType);
53
56
  }
54
57
  static fromBase64(base64, mimeType) {
55
- return new Image(base64, mimeType);
58
+ assertBase64(base64);
59
+ const normalizedMimeType = mimeType.toLowerCase();
60
+ if (!SUPPORTED_IMAGE_MIMES.has(normalizedMimeType)) {
61
+ throw new Error(`Unsupported image MIME type: ${normalizedMimeType}`);
62
+ }
63
+ return new Image(base64, normalizedMimeType);
56
64
  }
57
65
  toContentBlock() {
58
66
  return {
@@ -1 +1,7 @@
1
1
  export { fileTypeFromBuffer, type FileTypeResult } from "./file-type.js";
2
+ export declare function parseContentType(value: string | null): {
3
+ mimeType?: string;
4
+ charset?: string;
5
+ };
6
+ export declare function assertBase64(value: string): void;
7
+ export declare function safeRemoteLabel(url: string): string;
@@ -1 +1,52 @@
1
1
  export { fileTypeFromBuffer } from "./file-type.js";
2
+ export function parseContentType(value) {
3
+ if (!value) {
4
+ return {};
5
+ }
6
+ const [rawMimeType, ...parameters] = value.split(";");
7
+ const mimeType = rawMimeType?.trim().toLowerCase();
8
+ const charsetParameter = parameters.find((parameter) => parameter.trim().toLowerCase().startsWith("charset="));
9
+ const rawCharset = charsetParameter?.split("=", 2)[1]?.trim();
10
+ const charset = rawCharset?.startsWith('"') && rawCharset.endsWith('"')
11
+ ? rawCharset.slice(1, -1)
12
+ : rawCharset;
13
+ return {
14
+ ...(mimeType ? { mimeType } : {}),
15
+ ...(charset ? { charset } : {})
16
+ };
17
+ }
18
+ export function assertBase64(value) {
19
+ if (value.length % 4 !== 0) {
20
+ throw new Error("Invalid base64 content");
21
+ }
22
+ let paddingStarted = false;
23
+ let paddingCount = 0;
24
+ for (const character of value) {
25
+ if (character === "=") {
26
+ paddingStarted = true;
27
+ paddingCount += 1;
28
+ continue;
29
+ }
30
+ const code = character.charCodeAt(0);
31
+ const isValid = (code >= 65 && code <= 90) ||
32
+ (code >= 97 && code <= 122) ||
33
+ (code >= 48 && code <= 57) ||
34
+ character === "+" ||
35
+ character === "/";
36
+ if (!isValid || paddingStarted) {
37
+ throw new Error("Invalid base64 content");
38
+ }
39
+ }
40
+ if (paddingCount > 2) {
41
+ throw new Error("Invalid base64 content");
42
+ }
43
+ }
44
+ export function safeRemoteLabel(url) {
45
+ try {
46
+ const parsed = new URL(url);
47
+ return `${parsed.origin}${parsed.pathname}`;
48
+ }
49
+ catch {
50
+ return "remote resource";
51
+ }
52
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { createServer } from "./server.js";
2
- export type { Server } from "./server.js";
2
+ export type { MessageHandler, MessageSession, Server } from "./server.js";
3
3
  export { defineSchema } from "./schema.js";
4
4
  export type { TypedSchema } from "./schema.js";
5
5
  export { Image, Audio, File, toContentBlocks, fileTypeFromBuffer, } from "./content/index.js";
6
6
  export type { ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, } from "./content/index.js";
7
7
  export type { ToolReturn } from "./content/index.js";
8
- export type { ServerOptions, ToolHandler, ToolDefinition, Tool, CallToolResult, HandleResult, ContentItem, JSONSchema, JSONSchemaProperty, Transport, SDKTransport, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCMessage, JSONRPCNotification, InitializeResult, } from "./types.js";
8
+ export type { ServerOptions, ToolHandler, ToolDefinition, Tool, ToolAnnotations, ToolExecution, Icon, ContentAnnotations, ResourceLink, CallToolResult, PromptContentItem, PromptArgument, Prompt, PromptMessage, GetPromptResult, PromptHandler, PromptDefinition, Resource, ResourceTemplate, ResourceContents, ReadResourceResult, ResourceHandler, ResourceDefinition, ResourceTemplateDefinition, HandleResult, ContentItem, JSONSchema, JSONSchemaProperty, Transport, SDKTransport, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCMessage, JSONRPCNotification, InitializeResult, } from "./types.js";
9
9
  export { JSON_RPC_ERROR_CODES, ToolError } from "./types.js";
package/dist/jsonrpc.js CHANGED
@@ -28,7 +28,25 @@ export function parseMessage(line) {
28
28
  }
29
29
  const obj = parsed;
30
30
  const hasId = "id" in obj;
31
- const id = typeof obj.id === "string" || typeof obj.id === "number" ? obj.id : null;
31
+ const id = typeof obj.id === "string"
32
+ ? obj.id
33
+ : typeof obj.id === "number" && Number.isFinite(obj.id)
34
+ ? obj.id
35
+ : obj.id === null
36
+ ? null
37
+ : null;
38
+ if ("params" in obj
39
+ && (typeof obj.params !== "object"
40
+ || obj.params === null)) {
41
+ return {
42
+ success: false,
43
+ error: {
44
+ code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
45
+ message: "Invalid Request",
46
+ },
47
+ id,
48
+ };
49
+ }
32
50
  if (obj.jsonrpc !== "2.0") {
33
51
  return {
34
52
  success: false,
@@ -60,7 +78,7 @@ export function parseMessage(line) {
60
78
  },
61
79
  };
62
80
  }
63
- if (id === null) {
81
+ if (obj.id !== null && id === null) {
64
82
  return {
65
83
  success: false,
66
84
  error: {
package/dist/schema.js CHANGED
@@ -2,10 +2,15 @@ export function defineSchema(definition) {
2
2
  const properties = {};
3
3
  const required = [];
4
4
  for (const [key, prop] of Object.entries(definition)) {
5
- properties[key] = {
6
- type: prop.type,
7
- ...(prop.description !== undefined && { description: prop.description }),
8
- };
5
+ Object.defineProperty(properties, key, {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: {
10
+ type: prop.type,
11
+ ...(prop.description !== undefined && { description: prop.description }),
12
+ },
13
+ });
9
14
  if (!prop.optional) {
10
15
  required.push(key);
11
16
  }
package/dist/server.d.ts CHANGED
@@ -1,13 +1,29 @@
1
- import type { ServerOptions, ToolHandler, HandleResult, Transport, SDKTransport, JSONRPCNotification } from "./types.js";
1
+ import type { ServerOptions, ToolDefinition, ToolHandler, HandleResult, Prompt, PromptHandler, Resource, ResourceHandler, ResourceTemplate, Transport, SDKTransport, JSONRPCNotification } from "./types.js";
2
2
  import type { TypedSchema } from "./schema.js";
3
3
  export interface Server {
4
4
  tool<T>(name: string, description: string, inputSchema: TypedSchema<T>, handler: ToolHandler<T>): Server;
5
+ registerTool<T>(definition: Omit<ToolDefinition<T>, "handler">, handler: ToolHandler<T>): Server;
6
+ prompt(definition: Prompt, handler: PromptHandler): Server;
7
+ resource(definition: Resource, handler: ResourceHandler): Server;
8
+ resourceTemplate(definition: ResourceTemplate, handler: ResourceHandler): Server;
5
9
  onNotification(listener: (notification: JSONRPCNotification) => void): () => void;
6
10
  removeTool(name: string): boolean;
11
+ removePrompt(name: string): boolean;
12
+ removeResource(uri: string): boolean;
13
+ removeResourceTemplate(uriTemplate: string): boolean;
7
14
  notifyToolsChanged(): Promise<void>;
15
+ notifyPromptsChanged(): Promise<void>;
16
+ notifyResourcesChanged(): Promise<void>;
17
+ notifyResourceUpdated(uri: string): Promise<void>;
18
+ createMessageSession(listener?: (notification: JSONRPCNotification) => void | Promise<void>): MessageSession;
8
19
  handleMessage(method: string, params?: Record<string, unknown>): Promise<HandleResult>;
9
20
  listen(): Promise<void>;
10
21
  connect(transport: Transport): Promise<void>;
11
22
  connectSDK(transport: SDKTransport): Promise<void>;
12
23
  }
24
+ export type MessageHandler = (method: string, params?: Record<string, unknown>) => Promise<HandleResult>;
25
+ export interface MessageSession {
26
+ handleMessage: MessageHandler;
27
+ close(): void;
28
+ }
13
29
  export declare function createServer(options: ServerOptions): Server;
package/dist/server.js CHANGED
@@ -1,27 +1,65 @@
1
1
  import * as readline from "readline";
2
+ import AjvModule from "ajv";
3
+ import uriTemplateParser from "uri-template";
4
+ import UriTemplate from "uri-template-lite";
2
5
  import { JSON_RPC_ERROR_CODES, ToolError } from "./types.js";
3
6
  import { parseMessage, formatSuccessResponse, formatErrorResponse, } from "./jsonrpc.js";
4
7
  import { toContentBlocks } from "./content/convert.js";
5
8
  const PROTOCOL_VERSION = "2025-11-25";
9
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set([
10
+ "2025-03-26",
11
+ "2025-06-18",
12
+ PROTOCOL_VERSION,
13
+ ]);
6
14
  export function createServer(options) {
15
+ const Ajv = "default" in AjvModule ? AjvModule.default : AjvModule;
16
+ const jsonSchemaValidator = new Ajv({ strict: false });
17
+ const supportNotifications = options.supportNotifications !== false;
18
+ const supportResourceSubscriptions = options.supportResourceSubscriptions !== false;
7
19
  const tools = new Map();
20
+ const prompts = new Map();
21
+ const resources = new Map();
22
+ const resourceTemplates = new Map();
8
23
  const notificationListeners = new Set();
9
- let initialized = false;
10
- const handleMessage = async (method, params) => {
24
+ const connectionNotificationListeners = new Map();
25
+ const defaultLifecycle = {
26
+ initialized: false,
27
+ initializeAccepted: false,
28
+ notificationReady: false,
29
+ resourceSubscriptions: new Set(),
30
+ };
31
+ const messageLifecycles = new Set([defaultLifecycle]);
32
+ const handleMessageWithLifecycle = async (method, lifecycle, params) => {
11
33
  // Allow ping and initialize before initialization
12
34
  if (method === "ping") {
13
35
  return { result: {} };
14
36
  }
15
37
  if (method === "initialize") {
16
- initialized = true;
38
+ // Re-initialize on the same connection is idempotent: real MCP clients
39
+ // (e.g. kimi-cli via fastmcp) re-send `initialize` on a persistent
40
+ // connection per tool call, and the official MCP SDK server re-responds
41
+ // with InitializeResult instead of erroring. Per-connection isolation is
42
+ // still enforced by the separate lifecycle object given to each connection.
43
+ lifecycle.initializeAccepted = true;
44
+ lifecycle.initialized = true;
45
+ lifecycle.notificationReady = false;
17
46
  const requestedProtocol = typeof params?.protocolVersion === "string"
18
47
  ? params.protocolVersion
19
- : null;
48
+ : undefined;
20
49
  const result = {
21
- protocolVersion: requestedProtocol ?? PROTOCOL_VERSION,
50
+ protocolVersion: requestedProtocol !== undefined && SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol)
51
+ ? requestedProtocol
52
+ : PROTOCOL_VERSION,
22
53
  capabilities: {
23
54
  tools: {
24
- listChanged: true,
55
+ ...(supportNotifications ? { listChanged: true } : {}),
56
+ },
57
+ prompts: {
58
+ ...(supportNotifications ? { listChanged: true } : {}),
59
+ },
60
+ resources: {
61
+ ...(supportNotifications ? { listChanged: true } : {}),
62
+ ...(supportResourceSubscriptions ? { subscribe: true } : {}),
25
63
  },
26
64
  },
27
65
  serverInfo: {
@@ -32,10 +70,19 @@ export function createServer(options) {
32
70
  return { result };
33
71
  }
34
72
  if (method === "notifications/initialized") {
73
+ if (!lifecycle.initializeAccepted) {
74
+ return {
75
+ error: {
76
+ code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
77
+ message: "Server not initialized",
78
+ },
79
+ };
80
+ }
81
+ lifecycle.notificationReady = true;
35
82
  return { result: undefined };
36
83
  }
37
84
  // All other methods require initialization
38
- if (!initialized) {
85
+ if (!lifecycle.initialized) {
39
86
  return {
40
87
  error: {
41
88
  code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
@@ -46,17 +93,16 @@ export function createServer(options) {
46
93
  if (method === "tools/list") {
47
94
  const toolList = [];
48
95
  for (const tool of tools.values()) {
96
+ const descriptor = { ...tool };
97
+ delete descriptor.handler;
49
98
  toolList.push({
50
- name: tool.name,
51
- description: tool.description,
52
- inputSchema: tool.inputSchema,
99
+ ...descriptor,
53
100
  });
54
101
  }
55
102
  return { result: { tools: toolList } };
56
103
  }
57
104
  if (method === "tools/call") {
58
105
  const toolName = params?.name;
59
- const toolArgs = params?.arguments || {};
60
106
  if (!toolName) {
61
107
  return {
62
108
  error: {
@@ -74,9 +120,28 @@ export function createServer(options) {
74
120
  },
75
121
  };
76
122
  }
123
+ const toolArgs = (params?.arguments ?? {});
124
+ if (options.validateToolArguments !== false && !jsonSchemaValidator.validate(tool.inputSchema, toolArgs)) {
125
+ return {
126
+ error: {
127
+ code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
128
+ message: "Invalid tool arguments",
129
+ },
130
+ };
131
+ }
77
132
  try {
78
133
  const handlerResult = await tool.handler(toolArgs);
79
- const result = { content: toContentBlocks(handlerResult) };
134
+ if (hasContentArray(handlerResult) && !isCallToolResult(handlerResult)) {
135
+ throw new Error("Invalid tool result");
136
+ }
137
+ const result = isCallToolResult(handlerResult)
138
+ ? handlerResult
139
+ : { content: toContentBlocks(handlerResult) };
140
+ if (tool.outputSchema !== undefined
141
+ && (result.structuredContent === undefined
142
+ || !jsonSchemaValidator.validate(tool.outputSchema, result.structuredContent))) {
143
+ throw new Error("Invalid structured tool result");
144
+ }
80
145
  return { result };
81
146
  }
82
147
  catch (err) {
@@ -96,6 +161,96 @@ export function createServer(options) {
96
161
  return { result };
97
162
  }
98
163
  }
164
+ if (method === "prompts/list") {
165
+ return {
166
+ result: {
167
+ prompts: [...prompts.values()].map(({ handler: _handler, ...prompt }) => prompt),
168
+ },
169
+ };
170
+ }
171
+ if (method === "prompts/get") {
172
+ const promptName = typeof params?.name === "string" ? params.name : undefined;
173
+ if (promptName === undefined) {
174
+ return invalidParams("Prompt name required");
175
+ }
176
+ const prompt = prompts.get(promptName);
177
+ if (prompt === undefined) {
178
+ return invalidParams(`Prompt not found: ${promptName}`);
179
+ }
180
+ const args = toStringArguments(params?.arguments);
181
+ if (args === undefined || !hasRequiredPromptArguments(prompt, args)) {
182
+ return invalidParams("Invalid prompt arguments");
183
+ }
184
+ try {
185
+ const result = await prompt.handler(args);
186
+ if (!isGetPromptResult(result)) {
187
+ return internalError("Invalid prompt result");
188
+ }
189
+ return { result };
190
+ }
191
+ catch (error) {
192
+ return internalError(toErrorMessage(error));
193
+ }
194
+ }
195
+ if (method === "resources/list") {
196
+ return {
197
+ result: {
198
+ resources: [...resources.values()].map(({ handler: _handler, ...resource }) => resource),
199
+ },
200
+ };
201
+ }
202
+ if (method === "resources/templates/list") {
203
+ return {
204
+ result: {
205
+ resourceTemplates: [...resourceTemplates.values()].map(({ handler: _handler, ...resourceTemplate }) => resourceTemplate),
206
+ },
207
+ };
208
+ }
209
+ if (method === "resources/read") {
210
+ const uri = typeof params?.uri === "string" ? params.uri : undefined;
211
+ if (uri === undefined || !isValidUri(uri)) {
212
+ return invalidParams("Resource URI required");
213
+ }
214
+ const resource = findReadableResource(uri, resources, resourceTemplates);
215
+ if (resource === undefined) {
216
+ return resourceNotFound(uri);
217
+ }
218
+ try {
219
+ const result = await resource.handler(uri);
220
+ if (!isReadResourceResult(result)) {
221
+ return internalError("Invalid resource result");
222
+ }
223
+ return { result };
224
+ }
225
+ catch (error) {
226
+ return internalError(toErrorMessage(error));
227
+ }
228
+ }
229
+ if (method === "resources/subscribe" || method === "resources/unsubscribe") {
230
+ if (!supportResourceSubscriptions) {
231
+ return {
232
+ error: {
233
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
234
+ message: "Method not found",
235
+ },
236
+ };
237
+ }
238
+ const uri = typeof params?.uri === "string" ? params.uri : undefined;
239
+ if (uri === undefined || !isValidUri(uri)) {
240
+ return invalidParams("Resource URI required");
241
+ }
242
+ if (method === "resources/subscribe"
243
+ && findReadableResource(uri, resources, resourceTemplates) === undefined) {
244
+ return resourceNotFound(uri);
245
+ }
246
+ if (method === "resources/subscribe") {
247
+ lifecycle.resourceSubscriptions.add(uri);
248
+ }
249
+ else {
250
+ lifecycle.resourceSubscriptions.delete(uri);
251
+ }
252
+ return { result: {} };
253
+ }
99
254
  return {
100
255
  error: {
101
256
  code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
@@ -103,14 +258,47 @@ export function createServer(options) {
103
258
  },
104
259
  };
105
260
  };
106
- const processLine = async (line, write) => {
261
+ const createMessageSession = (listener) => {
262
+ const lifecycle = {
263
+ initialized: false,
264
+ initializeAccepted: false,
265
+ notificationReady: false,
266
+ resourceSubscriptions: new Set(),
267
+ };
268
+ messageLifecycles.add(lifecycle);
269
+ if (listener !== undefined) {
270
+ connectionNotificationListeners.set(listener, lifecycle);
271
+ }
272
+ return {
273
+ handleMessage: (method, params) => handleMessageWithLifecycle(method, lifecycle, params),
274
+ close: () => {
275
+ if (listener !== undefined) {
276
+ connectionNotificationListeners.delete(listener);
277
+ }
278
+ messageLifecycles.delete(lifecycle);
279
+ },
280
+ };
281
+ };
282
+ const handleMessage = (method, params) => handleMessageWithLifecycle(method, defaultLifecycle, params);
283
+ const processLine = async (line, write, messageHandler) => {
107
284
  const parsed = parseMessage(line);
108
285
  if (!parsed.success) {
109
286
  write(formatErrorResponse(parsed.id, parsed.error) + "\n");
110
287
  return;
111
288
  }
112
289
  const { request, isNotification } = parsed;
113
- const { result, error } = await server.handleMessage(request.method, request.params);
290
+ if (isNotification && request.method === "initialize") {
291
+ return;
292
+ }
293
+ if (!isNotification && request.method === "notifications/initialized") {
294
+ const requestWithId = request;
295
+ write(formatErrorResponse(requestWithId.id, {
296
+ code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
297
+ message: "Invalid Request",
298
+ }) + "\n");
299
+ return;
300
+ }
301
+ const { result, error } = await messageHandler(request.method, request.params);
114
302
  if (isNotification) {
115
303
  return;
116
304
  }
@@ -122,14 +310,20 @@ export function createServer(options) {
122
310
  write(formatSuccessResponse(requestWithId.id, result) + "\n");
123
311
  }
124
312
  };
125
- const broadcastNotification = (method) => {
313
+ const broadcastNotification = async (method, params, canSend = () => true) => {
126
314
  const notification = {
127
315
  jsonrpc: "2.0",
128
316
  method,
317
+ ...(params === undefined ? {} : { params }),
129
318
  };
130
319
  for (const listener of notificationListeners) {
131
320
  listener(notification);
132
321
  }
322
+ await Promise.all([...connectionNotificationListeners].map(async ([listener, lifecycle]) => {
323
+ if (lifecycle.notificationReady && canSend(lifecycle)) {
324
+ await listener(notification);
325
+ }
326
+ }));
133
327
  };
134
328
  const server = {
135
329
  tool(name, description, inputSchema, handler) {
@@ -141,6 +335,30 @@ export function createServer(options) {
141
335
  });
142
336
  return server;
143
337
  },
338
+ registerTool(definition, handler) {
339
+ tools.set(definition.name, {
340
+ ...definition,
341
+ handler: handler,
342
+ });
343
+ return server;
344
+ },
345
+ prompt(definition, handler) {
346
+ prompts.set(definition.name, { ...definition, handler });
347
+ return server;
348
+ },
349
+ resource(definition, handler) {
350
+ if (!isValidUri(definition.uri)) {
351
+ throw new Error(`Invalid resource URI: ${definition.uri}`);
352
+ }
353
+ resources.set(definition.uri, { ...definition, handler });
354
+ return server;
355
+ },
356
+ resourceTemplate(definition, handler) {
357
+ uriTemplateParser.parse(definition.uriTemplate);
358
+ new UriTemplate(definition.uriTemplate);
359
+ resourceTemplates.set(definition.uriTemplate, { ...definition, handler });
360
+ return server;
361
+ },
144
362
  onNotification(listener) {
145
363
  notificationListeners.add(listener);
146
364
  return () => {
@@ -150,11 +368,37 @@ export function createServer(options) {
150
368
  removeTool(name) {
151
369
  return tools.delete(name);
152
370
  },
371
+ removePrompt(name) {
372
+ return prompts.delete(name);
373
+ },
374
+ removeResource(uri) {
375
+ return resources.delete(uri);
376
+ },
377
+ removeResourceTemplate(uriTemplate) {
378
+ return resourceTemplates.delete(uriTemplate);
379
+ },
153
380
  async notifyToolsChanged() {
154
- if (initialized) {
155
- broadcastNotification("notifications/tools/list_changed");
381
+ if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
382
+ await broadcastNotification("notifications/tools/list_changed");
156
383
  }
157
384
  },
385
+ async notifyPromptsChanged() {
386
+ if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
387
+ await broadcastNotification("notifications/prompts/list_changed");
388
+ }
389
+ },
390
+ async notifyResourcesChanged() {
391
+ if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
392
+ await broadcastNotification("notifications/resources/list_changed");
393
+ }
394
+ },
395
+ async notifyResourceUpdated(uri) {
396
+ if (!supportResourceSubscriptions) {
397
+ return;
398
+ }
399
+ await broadcastNotification("notifications/resources/updated", { uri }, (lifecycle) => lifecycle.resourceSubscriptions.has(uri));
400
+ },
401
+ createMessageSession,
158
402
  handleMessage,
159
403
  async listen() {
160
404
  return server.connect({
@@ -164,27 +408,40 @@ export function createServer(options) {
164
408
  },
165
409
  async connect(transport) {
166
410
  return new Promise((resolve) => {
167
- const unsubscribe = server.onNotification((notification) => {
411
+ const lifecycle = { initialized: false, initializeAccepted: false, notificationReady: false, resourceSubscriptions: new Set() };
412
+ const messageHandler = (method, params) => handleMessageWithLifecycle(method, lifecycle, params);
413
+ messageLifecycles.add(lifecycle);
414
+ const listener = (notification) => {
168
415
  transport.writable.write(`${JSON.stringify(notification)}\n`);
169
- });
416
+ };
417
+ connectionNotificationListeners.set(listener, lifecycle);
170
418
  const rl = readline.createInterface({
171
419
  input: transport.readable,
172
420
  crlfDelay: Infinity,
173
421
  });
422
+ const pendingMessages = new Set();
174
423
  rl.on("line", (line) => {
175
- processLine(line, (data) => transport.writable.write(data));
424
+ const message = processLine(line, (data) => transport.writable.write(data), messageHandler);
425
+ pendingMessages.add(message);
426
+ void message.finally(() => {
427
+ pendingMessages.delete(message);
428
+ });
176
429
  });
177
- rl.on("close", () => {
178
- unsubscribe();
430
+ rl.on("close", async () => {
431
+ await Promise.all([...pendingMessages]);
432
+ connectionNotificationListeners.delete(listener);
433
+ messageLifecycles.delete(lifecycle);
179
434
  resolve();
180
435
  });
181
436
  });
182
437
  },
183
438
  async connectSDK(transport) {
184
- return new Promise((resolve) => {
185
- const unsubscribe = server.onNotification((notification) => {
186
- void transport.send(notification);
187
- });
439
+ return new Promise((resolve, reject) => {
440
+ const lifecycle = { initialized: false, initializeAccepted: false, notificationReady: false, resourceSubscriptions: new Set() };
441
+ const messageHandler = (method, params) => handleMessageWithLifecycle(method, lifecycle, params);
442
+ messageLifecycles.add(lifecycle);
443
+ const listener = (notification) => transport.send(notification);
444
+ connectionNotificationListeners.set(listener, lifecycle);
188
445
  transport.onmessage = async (message) => {
189
446
  // Ignore responses (we only handle requests/notifications)
190
447
  if (!("method" in message)) {
@@ -192,11 +449,25 @@ export function createServer(options) {
192
449
  }
193
450
  // Handle notifications (no id) - don't respond
194
451
  if (!("id" in message) || message.id === undefined) {
195
- await server.handleMessage(message.method, message.params);
452
+ if (message.method === "initialize") {
453
+ return;
454
+ }
455
+ await messageHandler(message.method, message.params);
456
+ return;
457
+ }
458
+ if (message.method === "notifications/initialized") {
459
+ await transport.send({
460
+ jsonrpc: "2.0",
461
+ id: message.id,
462
+ error: {
463
+ code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
464
+ message: "Invalid Request",
465
+ },
466
+ });
196
467
  return;
197
468
  }
198
469
  const request = message;
199
- const { result, error } = await server.handleMessage(request.method, request.params);
470
+ const { result, error } = await messageHandler(request.method, request.params);
200
471
  if (error) {
201
472
  const response = {
202
473
  jsonrpc: "2.0",
@@ -215,12 +486,166 @@ export function createServer(options) {
215
486
  }
216
487
  };
217
488
  transport.onclose = () => {
218
- unsubscribe();
489
+ connectionNotificationListeners.delete(listener);
490
+ messageLifecycles.delete(lifecycle);
219
491
  resolve();
220
492
  };
221
- transport.start();
493
+ void transport.start().catch((error) => {
494
+ connectionNotificationListeners.delete(listener);
495
+ messageLifecycles.delete(lifecycle);
496
+ reject(error);
497
+ });
222
498
  });
223
499
  },
224
500
  };
225
501
  return server;
226
502
  }
503
+ function invalidParams(message) {
504
+ return {
505
+ error: {
506
+ code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
507
+ message,
508
+ },
509
+ };
510
+ }
511
+ function internalError(message) {
512
+ return {
513
+ error: {
514
+ code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
515
+ message,
516
+ },
517
+ };
518
+ }
519
+ function resourceNotFound(uri) {
520
+ return {
521
+ error: {
522
+ code: JSON_RPC_ERROR_CODES.RESOURCE_NOT_FOUND,
523
+ message: `Resource not found: ${uri}`,
524
+ },
525
+ };
526
+ }
527
+ function toErrorMessage(error) {
528
+ return error instanceof Error ? error.message : String(error);
529
+ }
530
+ function isValidUri(uri) {
531
+ try {
532
+ new URL(uri);
533
+ return true;
534
+ }
535
+ catch {
536
+ return false;
537
+ }
538
+ }
539
+ function toStringArguments(value) {
540
+ if (value === undefined) {
541
+ return {};
542
+ }
543
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
544
+ return undefined;
545
+ }
546
+ const args = {};
547
+ for (const [name, argument] of Object.entries(value)) {
548
+ if (typeof argument !== "string") {
549
+ return undefined;
550
+ }
551
+ args[name] = argument;
552
+ }
553
+ return args;
554
+ }
555
+ function hasRequiredPromptArguments(prompt, args) {
556
+ return (prompt.arguments ?? []).every((argument) => argument.required !== true || args[argument.name] !== undefined);
557
+ }
558
+ function findReadableResource(uri, resources, resourceTemplates) {
559
+ const resource = resources.get(uri);
560
+ if (resource !== undefined) {
561
+ return resource;
562
+ }
563
+ return [...resourceTemplates.values()].find((template) => matchesUriTemplate(template.uriTemplate, uri));
564
+ }
565
+ function matchesUriTemplate(template, uri) {
566
+ try {
567
+ return new UriTemplate(template).match(uri) !== null;
568
+ }
569
+ catch {
570
+ return false;
571
+ }
572
+ }
573
+ function isCallToolResult(value) {
574
+ return hasContentArray(value) && value.content.every(isContentItem);
575
+ }
576
+ function isGetPromptResult(value) {
577
+ if (typeof value !== "object" || value === null || !("messages" in value)) {
578
+ return false;
579
+ }
580
+ return Array.isArray(value.messages)
581
+ && value.messages.every((message) => typeof message === "object"
582
+ && message !== null
583
+ && "role" in message
584
+ && (message.role === "user" || message.role === "assistant")
585
+ && "content" in message
586
+ && isPromptContentItem(message.content));
587
+ }
588
+ function isReadResourceResult(value) {
589
+ if (typeof value !== "object" || value === null || !("contents" in value)) {
590
+ return false;
591
+ }
592
+ return Array.isArray(value.contents)
593
+ && value.contents.every((content) => typeof content === "object"
594
+ && content !== null
595
+ && "uri" in content
596
+ && typeof content.uri === "string"
597
+ && isValidUri(content.uri)
598
+ && (("text" in content && typeof content.text === "string")
599
+ || ("blob" in content && typeof content.blob === "string" && isBase64(content.blob))));
600
+ }
601
+ function hasContentArray(value) {
602
+ return typeof value === "object" && value !== null && "content" in value
603
+ && Array.isArray(value.content);
604
+ }
605
+ function isContentItem(value) {
606
+ if (typeof value !== "object" || value === null || !("type" in value)) {
607
+ return false;
608
+ }
609
+ const block = value;
610
+ if (block.type === "text") {
611
+ return typeof block.text === "string";
612
+ }
613
+ if (block.type === "image" || block.type === "audio") {
614
+ return typeof block.data === "string" && isBase64(block.data) && typeof block.mimeType === "string";
615
+ }
616
+ if (block.type === "resource_link") {
617
+ return typeof block.uri === "string" && typeof block.name === "string";
618
+ }
619
+ if (block.type !== "resource" || typeof block.resource !== "object" || block.resource === null) {
620
+ return false;
621
+ }
622
+ const resource = block.resource;
623
+ return typeof resource.uri === "string"
624
+ && (resource.mimeType === undefined || typeof resource.mimeType === "string")
625
+ && (typeof resource.text === "string" || (typeof resource.blob === "string" && isBase64(resource.blob)));
626
+ }
627
+ function isBase64(value) {
628
+ if (value.length === 0) {
629
+ return true;
630
+ }
631
+ if (value.length % 4 !== 0) {
632
+ return false;
633
+ }
634
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
635
+ const paddingStart = value.indexOf("=");
636
+ const encoded = paddingStart === -1 ? value : value.slice(0, paddingStart);
637
+ const padding = paddingStart === -1 ? "" : value.slice(paddingStart);
638
+ if (padding.length > 2 || [...padding].some((character) => character !== "=")) {
639
+ return false;
640
+ }
641
+ if ([...encoded].some((character) => !alphabet.includes(character))) {
642
+ return false;
643
+ }
644
+ return Buffer.from(value, "base64").toString("base64") === value;
645
+ }
646
+ function isPromptContentItem(value) {
647
+ if (!isContentItem(value)) {
648
+ return false;
649
+ }
650
+ return !(typeof value === "object" && value !== null && "type" in value && value.type === "resource_link");
651
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export interface JSONRPCRequest {
2
2
  jsonrpc: "2.0";
3
- id: string | number;
3
+ id: string | number | null;
4
4
  method: string;
5
5
  params?: Record<string, unknown>;
6
6
  }
@@ -15,13 +15,14 @@ export interface JSONRPCError {
15
15
  message: string;
16
16
  data?: unknown;
17
17
  }
18
- export declare const JSON_RPC_ERROR_CODES: {
18
+ export declare const JSON_RPC_ERROR_CODES: Readonly<{
19
19
  readonly PARSE_ERROR: -32700;
20
20
  readonly INVALID_REQUEST: -32600;
21
21
  readonly METHOD_NOT_FOUND: -32601;
22
22
  readonly INVALID_PARAMS: -32602;
23
23
  readonly INTERNAL_ERROR: -32603;
24
- };
24
+ readonly RESOURCE_NOT_FOUND: -32002;
25
+ }>;
25
26
  export declare class ToolError extends Error {
26
27
  readonly code: number;
27
28
  constructor(code: number, message: string);
@@ -29,10 +30,19 @@ export declare class ToolError extends Error {
29
30
  export interface ToolsCapability {
30
31
  listChanged?: boolean;
31
32
  }
33
+ export interface PromptsCapability {
34
+ listChanged?: boolean;
35
+ }
36
+ export interface ResourcesCapability {
37
+ subscribe?: boolean;
38
+ listChanged?: boolean;
39
+ }
32
40
  export interface InitializeResult {
33
41
  protocolVersion: string;
34
42
  capabilities: {
35
43
  tools?: ToolsCapability;
44
+ prompts?: PromptsCapability;
45
+ resources?: ResourcesCapability;
36
46
  };
37
47
  serverInfo: {
38
48
  name: string;
@@ -41,13 +51,116 @@ export interface InitializeResult {
41
51
  }
42
52
  export interface Tool {
43
53
  name: string;
44
- description: string;
54
+ title?: string;
55
+ description?: string;
45
56
  inputSchema: JSONSchema;
57
+ outputSchema?: JSONSchema;
58
+ annotations?: ToolAnnotations;
59
+ execution?: ToolExecution;
60
+ icons?: Icon[];
61
+ _meta?: Record<string, unknown>;
46
62
  }
47
63
  export interface CallToolResult {
48
64
  content: ContentItem[];
65
+ structuredContent?: Record<string, unknown>;
49
66
  isError?: boolean;
50
67
  }
68
+ export interface ToolAnnotations {
69
+ title?: string;
70
+ readOnlyHint?: boolean;
71
+ destructiveHint?: boolean;
72
+ idempotentHint?: boolean;
73
+ openWorldHint?: boolean;
74
+ }
75
+ export interface ToolExecution {
76
+ taskSupport?: "optional" | "required" | "forbidden";
77
+ }
78
+ export interface Icon {
79
+ src: string;
80
+ mimeType?: string;
81
+ sizes?: string[];
82
+ theme?: "light" | "dark";
83
+ }
84
+ export interface ContentAnnotations {
85
+ audience?: Array<"user" | "assistant">;
86
+ priority?: number;
87
+ lastModified?: string;
88
+ }
89
+ export interface ResourceLink {
90
+ type: "resource_link";
91
+ uri: string;
92
+ name: string;
93
+ title?: string;
94
+ description?: string;
95
+ mimeType?: string;
96
+ size?: number;
97
+ annotations?: ContentAnnotations;
98
+ }
99
+ export interface PromptArgument {
100
+ name: string;
101
+ description?: string;
102
+ required?: boolean;
103
+ }
104
+ export interface Prompt {
105
+ name: string;
106
+ title?: string;
107
+ description?: string;
108
+ arguments?: PromptArgument[];
109
+ icons?: Icon[];
110
+ _meta?: Record<string, unknown>;
111
+ }
112
+ export interface PromptMessage {
113
+ role: "user" | "assistant";
114
+ content: PromptContentItem;
115
+ }
116
+ export interface GetPromptResult {
117
+ description?: string;
118
+ messages: PromptMessage[];
119
+ }
120
+ export type PromptHandler = (args: Record<string, string>) => Promise<GetPromptResult> | GetPromptResult;
121
+ export interface PromptDefinition extends Prompt {
122
+ handler: PromptHandler;
123
+ }
124
+ export interface Resource {
125
+ uri: string;
126
+ name: string;
127
+ title?: string;
128
+ description?: string;
129
+ mimeType?: string;
130
+ size?: number;
131
+ annotations?: ContentAnnotations;
132
+ icons?: Icon[];
133
+ _meta?: Record<string, unknown>;
134
+ }
135
+ export interface ResourceTemplate {
136
+ uriTemplate: string;
137
+ name: string;
138
+ title?: string;
139
+ description?: string;
140
+ mimeType?: string;
141
+ annotations?: ContentAnnotations;
142
+ icons?: Icon[];
143
+ _meta?: Record<string, unknown>;
144
+ }
145
+ export type ResourceContents = {
146
+ uri: string;
147
+ mimeType?: string;
148
+ text: string;
149
+ } | {
150
+ uri: string;
151
+ mimeType?: string;
152
+ blob: string;
153
+ };
154
+ export interface ReadResourceResult {
155
+ contents: ResourceContents[];
156
+ }
157
+ export type ResourceHandler = (uri: string) => Promise<ReadResourceResult> | ReadResourceResult;
158
+ export interface ResourceDefinition extends Resource {
159
+ handler: ResourceHandler;
160
+ }
161
+ export interface ResourceTemplateDefinition extends ResourceTemplate {
162
+ handler: ResourceHandler;
163
+ }
51
164
  export interface HandleResult {
52
165
  result?: unknown;
53
166
  error?: {
@@ -55,48 +168,64 @@ export interface HandleResult {
55
168
  message: string;
56
169
  };
57
170
  }
58
- export type ContentItem = {
171
+ export type PromptContentItem = {
59
172
  type: "text";
60
173
  text: string;
174
+ annotations?: ContentAnnotations;
61
175
  } | {
62
176
  type: "image";
63
177
  data: string;
64
178
  mimeType: string;
179
+ annotations?: ContentAnnotations;
65
180
  } | {
66
181
  type: "audio";
67
182
  data: string;
68
183
  mimeType: string;
184
+ annotations?: ContentAnnotations;
69
185
  } | {
70
186
  type: "resource";
187
+ annotations?: ContentAnnotations;
71
188
  resource: {
72
189
  uri: string;
73
- mimeType: string;
190
+ mimeType?: string;
74
191
  text: string;
75
192
  } | {
76
193
  uri: string;
77
- mimeType: string;
194
+ mimeType?: string;
78
195
  blob: string;
79
196
  };
80
197
  };
198
+ export type ContentItem = PromptContentItem | ResourceLink;
81
199
  export interface JSONSchema {
82
200
  type: "object";
83
- properties: Record<string, JSONSchemaProperty>;
201
+ properties?: Record<string, JSONSchemaProperty>;
84
202
  required?: string[];
203
+ [keyword: string]: unknown;
85
204
  }
86
- export interface JSONSchemaProperty {
87
- type: "string" | "number" | "boolean" | "object" | "array";
205
+ export interface JSONSchemaProperty extends Record<string, unknown> {
206
+ type?: string | string[];
88
207
  description?: string;
208
+ [keyword: string]: unknown;
89
209
  }
90
210
  export interface ServerOptions {
91
211
  name: string;
92
212
  version: string;
213
+ validateToolArguments?: boolean;
214
+ supportNotifications?: boolean;
215
+ supportResourceSubscriptions?: boolean;
93
216
  }
94
217
  import type { ToolReturn } from "./content/index.js";
95
- export type ToolHandler<T = Record<string, unknown>> = (args: T) => Promise<ToolReturn> | ToolReturn;
218
+ export type ToolHandler<T = Record<string, unknown>> = (args: T) => Promise<ToolReturn | CallToolResult> | ToolReturn | CallToolResult;
96
219
  export interface ToolDefinition<T = Record<string, unknown>> {
97
220
  name: string;
98
- description: string;
221
+ title?: string;
222
+ description?: string;
99
223
  inputSchema: JSONSchema;
224
+ outputSchema?: JSONSchema;
225
+ annotations?: ToolAnnotations;
226
+ execution?: ToolExecution;
227
+ icons?: Icon[];
228
+ _meta?: Record<string, unknown>;
100
229
  handler: ToolHandler<T>;
101
230
  }
102
231
  export interface Transport {
package/dist/types.js CHANGED
@@ -1,14 +1,18 @@
1
1
  // JSON-RPC error codes
2
- export const JSON_RPC_ERROR_CODES = {
2
+ export const JSON_RPC_ERROR_CODES = Object.freeze({
3
3
  PARSE_ERROR: -32700,
4
4
  INVALID_REQUEST: -32600,
5
5
  METHOD_NOT_FOUND: -32601,
6
6
  INVALID_PARAMS: -32602,
7
- INTERNAL_ERROR: -32603
8
- };
7
+ INTERNAL_ERROR: -32603,
8
+ RESOURCE_NOT_FOUND: -32002
9
+ });
9
10
  export class ToolError extends Error {
10
11
  code;
11
12
  constructor(code, message) {
13
+ if (!Number.isFinite(code)) {
14
+ throw new Error("ToolError code must be a finite number");
15
+ }
12
16
  super(message);
13
17
  this.code = code;
14
18
  this.name = "ToolError";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-stdio-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Minimal MCP server over stdio with typed tools and rich content helpers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "scripts": {
23
- "build": "tsc",
23
+ "build": "node ../../scripts/guard-package-dist.mjs && tsc",
24
24
  "prepublishOnly": "tsc"
25
25
  },
26
26
  "files": [
@@ -44,5 +44,10 @@
44
44
  ],
45
45
  "devDependencies": {
46
46
  "@modelcontextprotocol/sdk": "^1.25.3"
47
+ },
48
+ "dependencies": {
49
+ "ajv": "^8.20.0",
50
+ "uri-template": "^2.0.0",
51
+ "uri-template-lite": "^23.4.0"
47
52
  }
48
53
  }