tiny-stdio-mcp-server 0.1.2 → 0.1.4

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 {
@@ -32,11 +32,30 @@ export function toContentBlocks(result) {
32
32
  return [convertSingleValue(result)];
33
33
  }
34
34
  function isContentBlock(value) {
35
- if (!("type" in value) || typeof value.type !== "string") {
35
+ if (!hasOwnProperty(value, "type") || typeof value.type !== "string") {
36
36
  return false;
37
37
  }
38
- return (value.type === "text" ||
39
- value.type === "image" ||
40
- value.type === "audio" ||
41
- value.type === "resource");
38
+ if (value.type === "text") {
39
+ return hasOwnProperty(value, "text") && typeof value.text === "string";
40
+ }
41
+ if (value.type === "image" || value.type === "audio") {
42
+ return hasOwnProperty(value, "data")
43
+ && typeof value.data === "string"
44
+ && hasOwnProperty(value, "mimeType")
45
+ && typeof value.mimeType === "string";
46
+ }
47
+ if (value.type !== "resource" || !hasOwnProperty(value, "resource")) {
48
+ return false;
49
+ }
50
+ const resource = value.resource;
51
+ return typeof resource === "object"
52
+ && resource !== null
53
+ && hasOwnProperty(resource, "uri")
54
+ && typeof resource.uri === "string"
55
+ && (!hasOwnProperty(resource, "mimeType") || typeof resource.mimeType === "string")
56
+ && ((hasOwnProperty(resource, "text") && typeof resource.text === "string")
57
+ || (hasOwnProperty(resource, "blob") && typeof resource.blob === "string"));
58
+ }
59
+ function hasOwnProperty(value, name) {
60
+ return Object.prototype.hasOwnProperty.call(value, name);
42
61
  }
@@ -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,11 +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);
134
+ if (hasContentArray(handlerResult) && !isCallToolResult(handlerResult)) {
135
+ throw new Error("Invalid tool result");
136
+ }
79
137
  const result = isCallToolResult(handlerResult)
80
138
  ? handlerResult
81
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
+ }
82
145
  return { result };
83
146
  }
84
147
  catch (err) {
@@ -98,6 +161,96 @@ export function createServer(options) {
98
161
  return { result };
99
162
  }
100
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
+ }
101
254
  return {
102
255
  error: {
103
256
  code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
@@ -105,14 +258,47 @@ export function createServer(options) {
105
258
  },
106
259
  };
107
260
  };
108
- 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) => {
109
284
  const parsed = parseMessage(line);
110
285
  if (!parsed.success) {
111
286
  write(formatErrorResponse(parsed.id, parsed.error) + "\n");
112
287
  return;
113
288
  }
114
289
  const { request, isNotification } = parsed;
115
- 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);
116
302
  if (isNotification) {
117
303
  return;
118
304
  }
@@ -124,14 +310,20 @@ export function createServer(options) {
124
310
  write(formatSuccessResponse(requestWithId.id, result) + "\n");
125
311
  }
126
312
  };
127
- const broadcastNotification = (method) => {
313
+ const broadcastNotification = async (method, params, canSend = () => true) => {
128
314
  const notification = {
129
315
  jsonrpc: "2.0",
130
316
  method,
317
+ ...(params === undefined ? {} : { params }),
131
318
  };
132
319
  for (const listener of notificationListeners) {
133
320
  listener(notification);
134
321
  }
322
+ await Promise.all([...connectionNotificationListeners].map(async ([listener, lifecycle]) => {
323
+ if (lifecycle.notificationReady && canSend(lifecycle)) {
324
+ await listener(notification);
325
+ }
326
+ }));
135
327
  };
136
328
  const server = {
137
329
  tool(name, description, inputSchema, handler) {
@@ -143,6 +335,30 @@ export function createServer(options) {
143
335
  });
144
336
  return server;
145
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
+ },
146
362
  onNotification(listener) {
147
363
  notificationListeners.add(listener);
148
364
  return () => {
@@ -152,11 +368,37 @@ export function createServer(options) {
152
368
  removeTool(name) {
153
369
  return tools.delete(name);
154
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
+ },
155
380
  async notifyToolsChanged() {
156
- if (initialized) {
157
- broadcastNotification("notifications/tools/list_changed");
381
+ if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
382
+ await broadcastNotification("notifications/tools/list_changed");
383
+ }
384
+ },
385
+ async notifyPromptsChanged() {
386
+ if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
387
+ await broadcastNotification("notifications/prompts/list_changed");
158
388
  }
159
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,
160
402
  handleMessage,
161
403
  async listen() {
162
404
  return server.connect({
@@ -166,27 +408,40 @@ export function createServer(options) {
166
408
  },
167
409
  async connect(transport) {
168
410
  return new Promise((resolve) => {
169
- 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) => {
170
415
  transport.writable.write(`${JSON.stringify(notification)}\n`);
171
- });
416
+ };
417
+ connectionNotificationListeners.set(listener, lifecycle);
172
418
  const rl = readline.createInterface({
173
419
  input: transport.readable,
174
420
  crlfDelay: Infinity,
175
421
  });
422
+ const pendingMessages = new Set();
176
423
  rl.on("line", (line) => {
177
- 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
+ });
178
429
  });
179
- rl.on("close", () => {
180
- unsubscribe();
430
+ rl.on("close", async () => {
431
+ await Promise.all([...pendingMessages]);
432
+ connectionNotificationListeners.delete(listener);
433
+ messageLifecycles.delete(lifecycle);
181
434
  resolve();
182
435
  });
183
436
  });
184
437
  },
185
438
  async connectSDK(transport) {
186
- return new Promise((resolve) => {
187
- const unsubscribe = server.onNotification((notification) => {
188
- void transport.send(notification);
189
- });
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);
190
445
  transport.onmessage = async (message) => {
191
446
  // Ignore responses (we only handle requests/notifications)
192
447
  if (!("method" in message)) {
@@ -194,11 +449,25 @@ export function createServer(options) {
194
449
  }
195
450
  // Handle notifications (no id) - don't respond
196
451
  if (!("id" in message) || message.id === undefined) {
197
- 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
+ });
198
467
  return;
199
468
  }
200
469
  const request = message;
201
- const { result, error } = await server.handleMessage(request.method, request.params);
470
+ const { result, error } = await messageHandler(request.method, request.params);
202
471
  if (error) {
203
472
  const response = {
204
473
  jsonrpc: "2.0",
@@ -217,18 +486,181 @@ export function createServer(options) {
217
486
  }
218
487
  };
219
488
  transport.onclose = () => {
220
- unsubscribe();
489
+ connectionNotificationListeners.delete(listener);
490
+ messageLifecycles.delete(lifecycle);
221
491
  resolve();
222
492
  };
223
- transport.start();
493
+ void transport.start().catch((error) => {
494
+ connectionNotificationListeners.delete(listener);
495
+ messageLifecycles.delete(lifecycle);
496
+ reject(error);
497
+ });
224
498
  });
225
499
  },
226
500
  };
227
501
  return server;
228
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
+ }
229
573
  function isCallToolResult(value) {
230
- if (typeof value !== "object" || value === null || !("content" in value)) {
574
+ return hasContentArray(value) && value.content.every(isContentItem);
575
+ }
576
+ function isGetPromptResult(value) {
577
+ if (typeof value !== "object" || value === null || !hasOwnProperty(value, "messages")) {
578
+ return false;
579
+ }
580
+ return Array.isArray(value.messages)
581
+ && value.messages.every((message) => typeof message === "object"
582
+ && message !== null
583
+ && hasOwnProperty(message, "role")
584
+ && (message.role === "user" || message.role === "assistant")
585
+ && hasOwnProperty(message, "content")
586
+ && isPromptContentItem(message.content));
587
+ }
588
+ function isReadResourceResult(value) {
589
+ if (typeof value !== "object" || value === null || !hasOwnProperty(value, "contents")) {
590
+ return false;
591
+ }
592
+ return Array.isArray(value.contents)
593
+ && value.contents.every((content) => typeof content === "object"
594
+ && content !== null
595
+ && hasOwnProperty(content, "uri")
596
+ && typeof content.uri === "string"
597
+ && isValidUri(content.uri)
598
+ && ((hasOwnProperty(content, "text") && typeof content.text === "string")
599
+ || (hasOwnProperty(content, "blob") && typeof content.blob === "string" && isBase64(content.blob))));
600
+ }
601
+ function hasContentArray(value) {
602
+ return typeof value === "object" && value !== null && hasOwnProperty(value, "content")
603
+ && Array.isArray(value.content);
604
+ }
605
+ function isContentItem(value) {
606
+ if (typeof value !== "object" || value === null || !hasOwnProperty(value, "type")) {
607
+ return false;
608
+ }
609
+ const block = value;
610
+ if (block.type === "text") {
611
+ return hasOwnProperty(block, "text") && typeof block.text === "string";
612
+ }
613
+ if (block.type === "image" || block.type === "audio") {
614
+ return hasOwnProperty(block, "data")
615
+ && typeof block.data === "string"
616
+ && isBase64(block.data)
617
+ && hasOwnProperty(block, "mimeType")
618
+ && typeof block.mimeType === "string";
619
+ }
620
+ if (block.type === "resource_link") {
621
+ return hasOwnProperty(block, "uri")
622
+ && typeof block.uri === "string"
623
+ && hasOwnProperty(block, "name")
624
+ && typeof block.name === "string";
625
+ }
626
+ if (block.type !== "resource"
627
+ || !hasOwnProperty(block, "resource")
628
+ || typeof block.resource !== "object"
629
+ || block.resource === null) {
630
+ return false;
631
+ }
632
+ const resource = block.resource;
633
+ return hasOwnProperty(resource, "uri")
634
+ && typeof resource.uri === "string"
635
+ && (!hasOwnProperty(resource, "mimeType") || typeof resource.mimeType === "string")
636
+ && ((hasOwnProperty(resource, "text") && typeof resource.text === "string")
637
+ || (hasOwnProperty(resource, "blob") && typeof resource.blob === "string" && isBase64(resource.blob)));
638
+ }
639
+ function isBase64(value) {
640
+ if (value.length === 0) {
641
+ return true;
642
+ }
643
+ if (value.length % 4 !== 0) {
644
+ return false;
645
+ }
646
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
647
+ const paddingStart = value.indexOf("=");
648
+ const encoded = paddingStart === -1 ? value : value.slice(0, paddingStart);
649
+ const padding = paddingStart === -1 ? "" : value.slice(paddingStart);
650
+ if (padding.length > 2 || [...padding].some((character) => character !== "=")) {
651
+ return false;
652
+ }
653
+ if ([...encoded].some((character) => !alphabet.includes(character))) {
231
654
  return false;
232
655
  }
233
- return Array.isArray(value.content);
656
+ return Buffer.from(value, "base64").toString("base64") === value;
657
+ }
658
+ function isPromptContentItem(value) {
659
+ if (!isContentItem(value)) {
660
+ return false;
661
+ }
662
+ return !(typeof value === "object" && value !== null && hasOwnProperty(value, "type") && value.type === "resource_link");
663
+ }
664
+ function hasOwnProperty(value, name) {
665
+ return Object.prototype.hasOwnProperty.call(value, name);
234
666
  }
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
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.2",
3
+ "version": "0.1.4",
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
  }