tiny-stdio-mcp-server 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,7 @@ server.tool("search", "Search for things", schema, async ({ query, limit }) => {
58
58
  });
59
59
  ```
60
60
 
61
- For structured-data tools, pass a root-object output schema. The server advertises it as MCP `Tool.outputSchema`, validates the handler result, returns it as `CallToolResult.structuredContent`, and also includes a JSON text backstop in `content[]` for older clients.
61
+ For structured-data tools, pass a root-object output schema. The server advertises it as MCP `Tool.outputSchema`, validates successful handler results, and returns them as `CallToolResult.structuredContent`. Plain object returns receive a JSON text backstop in `content[]` for older clients. Explicit `CallToolResult` values keep their handler-supplied content blocks; the JSON text backstop is added only when `content` is empty.
62
62
 
63
63
  ```ts
64
64
  const input = defineSchema({
@@ -89,7 +89,11 @@ server.tool(
89
89
  );
90
90
  ```
91
91
 
92
- Output schemas must have `type: "object"` at the root. Tools whose natural result is prose, images, audio, files, or other content blocks should omit `outputSchema` and keep returning content.
92
+ Output schemas must have `type: "object"` at the root because MCP structured content is an object. Otherwise, any schema accepted by Ajv is supported, including composition keywords, nullable type unions, and local `$defs`/`$ref` references. Input and output schemas compile synchronously during `.tool()` or `.registerTool()` registration, so malformed schemas throw immediately instead of failing on the first tool call.
93
+
94
+ Successful structured results must satisfy `outputSchema`. Validation failures use JSON-RPC `-32602` for inputs and `-32603` for outputs, with Ajv's formatted error text in the message and the raw Ajv error array in `error.data`. Explicit handler results with `isError: true` are passed through unchanged and are exempt from structured-content and output-schema validation.
95
+
96
+ Tools whose natural result is prose, images, audio, files, or other content blocks should omit `outputSchema` and keep returning content.
93
97
 
94
98
  ### `.listen()`
95
99
 
package/dist/server.js CHANGED
@@ -47,7 +47,8 @@ export function createServer(options) {
47
47
  ? params.protocolVersion
48
48
  : undefined;
49
49
  const result = {
50
- protocolVersion: requestedProtocol !== undefined && SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol)
50
+ protocolVersion: requestedProtocol !== undefined &&
51
+ SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol)
51
52
  ? requestedProtocol
52
53
  : PROTOCOL_VERSION,
53
54
  capabilities: {
@@ -95,6 +96,9 @@ export function createServer(options) {
95
96
  for (const tool of tools.values()) {
96
97
  const descriptor = { ...tool };
97
98
  delete descriptor.handler;
99
+ delete descriptor.inputValidator;
100
+ delete descriptor
101
+ .outputValidator;
98
102
  toolList.push({
99
103
  ...descriptor,
100
104
  });
@@ -121,19 +125,25 @@ export function createServer(options) {
121
125
  };
122
126
  }
123
127
  const toolArgs = (params?.arguments ?? {});
124
- if (options.validateToolArguments !== false && !jsonSchemaValidator.validate(tool.inputSchema, toolArgs)) {
128
+ if (options.validateToolArguments !== false &&
129
+ !tool.inputValidator(toolArgs)) {
130
+ const errors = tool.inputValidator.errors ?? [];
125
131
  return {
126
132
  error: {
127
133
  code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
128
- message: "Invalid tool arguments",
134
+ message: `Invalid tool arguments: ${jsonSchemaValidator.errorsText(errors)}`,
135
+ data: errors,
129
136
  },
130
137
  };
131
138
  }
132
139
  try {
133
140
  const handlerResult = await tool.handler(toolArgs);
134
141
  const result = normalizeToolResult(handlerResult, tool.outputSchema);
135
- if (tool.outputSchema !== undefined && !jsonSchemaValidator.validate(tool.outputSchema, result.structuredContent)) {
136
- throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, "Invalid structured tool result");
142
+ if (result.isError !== true &&
143
+ tool.outputValidator !== undefined &&
144
+ !tool.outputValidator(result.structuredContent)) {
145
+ const errors = tool.outputValidator.errors ?? [];
146
+ throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Invalid structured tool result: ${jsonSchemaValidator.errorsText(errors)}`, errors);
137
147
  }
138
148
  return { result };
139
149
  }
@@ -220,7 +230,8 @@ export function createServer(options) {
220
230
  return internalError(toErrorMessage(error));
221
231
  }
222
232
  }
223
- if (method === "resources/subscribe" || method === "resources/unsubscribe") {
233
+ if (method === "resources/subscribe" ||
234
+ method === "resources/unsubscribe") {
224
235
  if (!supportResourceSubscriptions) {
225
236
  return {
226
237
  error: {
@@ -233,8 +244,8 @@ export function createServer(options) {
233
244
  if (uri === undefined || !isValidUri(uri)) {
234
245
  return invalidParams("Resource URI required");
235
246
  }
236
- if (method === "resources/subscribe"
237
- && findReadableResource(uri, resources, resourceTemplates) === undefined) {
247
+ if (method === "resources/subscribe" &&
248
+ findReadableResource(uri, resources, resourceTemplates) === undefined) {
238
249
  return resourceNotFound(uri);
239
250
  }
240
251
  if (method === "resources/subscribe") {
@@ -336,26 +347,38 @@ export function createServer(options) {
336
347
  const server = {
337
348
  tool(name, description, inputSchema, handler, outputSchema) {
338
349
  assertNonEmptyName(name, "Tool name required");
350
+ const inputValidator = jsonSchemaValidator.compile(inputSchema);
351
+ let outputValidator;
339
352
  if (outputSchema !== undefined) {
340
- assertSupportedOutputSchema(outputSchema);
353
+ assertObjectRootSchema(outputSchema, "outputSchema");
354
+ outputValidator = jsonSchemaValidator.compile(outputSchema);
341
355
  }
342
356
  tools.set(name, {
343
357
  name,
344
358
  description,
345
359
  inputSchema: inputSchema,
346
- ...(outputSchema === undefined ? {} : { outputSchema: outputSchema }),
360
+ ...(outputSchema === undefined
361
+ ? {}
362
+ : { outputSchema: outputSchema }),
347
363
  handler: handler,
364
+ inputValidator,
365
+ ...(outputValidator === undefined ? {} : { outputValidator }),
348
366
  });
349
367
  return server;
350
368
  },
351
369
  registerTool(definition, handler) {
352
370
  assertNonEmptyName(definition.name, "Tool name required");
371
+ const inputValidator = jsonSchemaValidator.compile(definition.inputSchema);
372
+ let outputValidator;
353
373
  if (definition.outputSchema !== undefined) {
354
- assertSupportedOutputSchema(definition.outputSchema);
374
+ assertObjectRootSchema(definition.outputSchema, "outputSchema");
375
+ outputValidator = jsonSchemaValidator.compile(definition.outputSchema);
355
376
  }
356
377
  tools.set(definition.name, {
357
378
  ...definition,
358
379
  handler: handler,
380
+ inputValidator,
381
+ ...(outputValidator === undefined ? {} : { outputValidator }),
359
382
  });
360
383
  return server;
361
384
  },
@@ -396,17 +419,20 @@ export function createServer(options) {
396
419
  return resourceTemplates.delete(uriTemplate);
397
420
  },
398
421
  async notifyToolsChanged() {
399
- if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
422
+ if (supportNotifications &&
423
+ [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
400
424
  await broadcastNotification("notifications/tools/list_changed");
401
425
  }
402
426
  },
403
427
  async notifyPromptsChanged() {
404
- if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
428
+ if (supportNotifications &&
429
+ [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
405
430
  await broadcastNotification("notifications/prompts/list_changed");
406
431
  }
407
432
  },
408
433
  async notifyResourcesChanged() {
409
- if (supportNotifications && [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
434
+ if (supportNotifications &&
435
+ [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
410
436
  await broadcastNotification("notifications/resources/list_changed");
411
437
  }
412
438
  },
@@ -617,14 +643,14 @@ function isCallToolResult(value) {
617
643
  if (!hasContentArray(value) || !value.content.every(isContentItem)) {
618
644
  return false;
619
645
  }
620
- if (hasOwnProperty(value, "structuredContent")
621
- && value.structuredContent !== undefined
622
- && !isJsonObject(value.structuredContent)) {
646
+ if (hasOwnProperty(value, "structuredContent") &&
647
+ value.structuredContent !== undefined &&
648
+ !isJsonObject(value.structuredContent)) {
623
649
  return false;
624
650
  }
625
- return !(hasOwnProperty(value, "isError")
626
- && value.isError !== undefined
627
- && typeof value.isError !== "boolean");
651
+ return !(hasOwnProperty(value, "isError") &&
652
+ value.isError !== undefined &&
653
+ typeof value.isError !== "boolean");
628
654
  }
629
655
  function normalizeToolResult(handlerResult, outputSchema) {
630
656
  if (hasContentArray(handlerResult) && !isCallToolResult(handlerResult)) {
@@ -639,131 +665,71 @@ function normalizeToolResult(handlerResult, outputSchema) {
639
665
  }
640
666
  return result;
641
667
  }
642
- const structuredContent = isCallToolResult(handlerResult)
643
- ? handlerResult.structuredContent
668
+ if (isCallToolResult(handlerResult) && handlerResult.isError === true) {
669
+ return handlerResult;
670
+ }
671
+ const callToolResult = isCallToolResult(handlerResult)
672
+ ? handlerResult
673
+ : undefined;
674
+ const structuredContent = callToolResult
675
+ ? callToolResult.structuredContent
644
676
  : handlerResult;
645
677
  if (!isJsonObject(structuredContent)) {
646
678
  throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, "Structured tool result must be an object");
647
679
  }
648
680
  return {
649
- content: [{ type: "text", text: JSON.stringify(structuredContent) }],
650
- ...(isCallToolResult(handlerResult) && handlerResult.isError !== undefined
651
- ? { isError: handlerResult.isError }
681
+ content: callToolResult !== undefined && callToolResult.content.length > 0
682
+ ? callToolResult.content
683
+ : [{ type: "text", text: JSON.stringify(structuredContent) }],
684
+ ...(callToolResult?.isError !== undefined
685
+ ? { isError: callToolResult.isError }
652
686
  : {}),
653
687
  structuredContent,
654
688
  };
655
689
  }
656
- function assertSupportedOutputSchema(schema) {
657
- assertObjectRootSchema(schema, "outputSchema");
658
- assertSupportedJsonSchema(schema, "outputSchema");
659
- }
660
690
  function assertObjectRootSchema(schema, path) {
661
691
  if (schema.type !== "object") {
662
692
  throw new Error(`${path} root type must be "object"`);
663
693
  }
664
694
  }
665
- function assertSupportedJsonSchema(schema, path) {
666
- for (const keyword of ["anyOf", "allOf", "not", "if", "then", "else", "contains", "prefixItems"]) {
667
- if (schema[keyword] !== undefined) {
668
- throw new Error(`${path} uses unsupported keyword "${keyword}"`);
669
- }
670
- }
671
- const type = schema.type;
672
- if (Array.isArray(type)) {
673
- const supported = new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
674
- for (const item of type) {
675
- if (!supported.has(item)) {
676
- throw new Error(`${path} uses unsupported type "${item}"`);
677
- }
678
- }
679
- }
680
- else if (type !== undefined &&
681
- type !== "string" &&
682
- type !== "number" &&
683
- type !== "integer" &&
684
- type !== "boolean" &&
685
- type !== "object" &&
686
- type !== "array") {
687
- throw new Error(`${path} uses unsupported type "${type}"`);
688
- }
689
- if (isJsonObject(schema.properties)) {
690
- for (const [key, child] of Object.entries(schema.properties)) {
691
- if (isJsonObject(child)) {
692
- assertSupportedJsonSchema(child, `${path}.properties.${key}`);
693
- }
694
- else {
695
- throw new Error(`${path}.properties.${key} must be an object schema`);
696
- }
697
- }
698
- }
699
- else if (schema.properties !== undefined) {
700
- throw new Error(`${path}.properties must be an object`);
701
- }
702
- const additionalProperties = schema.additionalProperties;
703
- if (typeof additionalProperties === "object" && additionalProperties !== null) {
704
- if (Array.isArray(additionalProperties)) {
705
- throw new Error(`${path}.additionalProperties must be an object schema or boolean`);
706
- }
707
- assertSupportedJsonSchema(additionalProperties, `${path}.additionalProperties`);
708
- }
709
- else if (additionalProperties !== undefined
710
- && typeof additionalProperties !== "boolean") {
711
- throw new Error(`${path}.additionalProperties must be an object schema or boolean`);
712
- }
713
- const items = schema.items;
714
- if (Array.isArray(items)) {
715
- throw new Error(`${path}.items uses unsupported tuple array schemas`);
716
- }
717
- if (typeof items === "object" && items !== null && !Array.isArray(items)) {
718
- assertSupportedJsonSchema(items, `${path}.items`);
719
- }
720
- else if (items !== undefined && typeof items !== "boolean") {
721
- throw new Error(`${path}.items must be an object schema or boolean`);
722
- }
723
- const oneOf = schema.oneOf;
724
- if (Array.isArray(oneOf)) {
725
- for (const [index, child] of oneOf.entries()) {
726
- if (typeof child === "object" && child !== null && !Array.isArray(child)) {
727
- assertSupportedJsonSchema(child, `${path}.oneOf[${index}]`);
728
- }
729
- else {
730
- throw new Error(`${path}.oneOf[${index}] must be an object schema`);
731
- }
732
- }
733
- }
734
- else if (oneOf !== undefined) {
735
- throw new Error(`${path}.oneOf must be an array`);
736
- }
737
- }
738
695
  function isJsonObject(value) {
739
696
  return typeof value === "object" && value !== null && !Array.isArray(value);
740
697
  }
741
698
  function isGetPromptResult(value) {
742
- if (typeof value !== "object" || value === null || !hasOwnProperty(value, "messages")) {
699
+ if (typeof value !== "object" ||
700
+ value === null ||
701
+ !hasOwnProperty(value, "messages")) {
743
702
  return false;
744
703
  }
745
- return (!hasOwnProperty(value, "description") || value.description === undefined || typeof value.description === "string")
746
- && Array.isArray(value.messages)
747
- && value.messages.every((message) => typeof message === "object"
748
- && message !== null
749
- && hasOwnProperty(message, "role")
750
- && (message.role === "user" || message.role === "assistant")
751
- && hasOwnProperty(message, "content")
752
- && isPromptContentItem(message.content));
704
+ return ((!hasOwnProperty(value, "description") ||
705
+ value.description === undefined ||
706
+ typeof value.description === "string") &&
707
+ Array.isArray(value.messages) &&
708
+ value.messages.every((message) => typeof message === "object" &&
709
+ message !== null &&
710
+ hasOwnProperty(message, "role") &&
711
+ (message.role === "user" || message.role === "assistant") &&
712
+ hasOwnProperty(message, "content") &&
713
+ isPromptContentItem(message.content)));
753
714
  }
754
715
  function isReadResourceResult(value) {
755
- if (typeof value !== "object" || value === null || !hasOwnProperty(value, "contents")) {
716
+ if (typeof value !== "object" ||
717
+ value === null ||
718
+ !hasOwnProperty(value, "contents")) {
756
719
  return false;
757
720
  }
758
- return Array.isArray(value.contents)
759
- && value.contents.every(isResourceContents);
721
+ return (Array.isArray(value.contents) && value.contents.every(isResourceContents));
760
722
  }
761
723
  function hasContentArray(value) {
762
- return typeof value === "object" && value !== null && hasOwnProperty(value, "content")
763
- && Array.isArray(value.content);
724
+ return (typeof value === "object" &&
725
+ value !== null &&
726
+ hasOwnProperty(value, "content") &&
727
+ Array.isArray(value.content));
764
728
  }
765
729
  function isContentItem(value) {
766
- if (typeof value !== "object" || value === null || !hasOwnProperty(value, "type")) {
730
+ if (typeof value !== "object" ||
731
+ value === null ||
732
+ !hasOwnProperty(value, "type")) {
767
733
  return false;
768
734
  }
769
735
  const block = value;
@@ -774,56 +740,71 @@ function isContentItem(value) {
774
740
  return hasOwnProperty(block, "text") && typeof block.text === "string";
775
741
  }
776
742
  if (block.type === "image" || block.type === "audio") {
777
- return hasOwnProperty(block, "data")
778
- && typeof block.data === "string"
779
- && isBase64(block.data)
780
- && hasOwnProperty(block, "mimeType")
781
- && typeof block.mimeType === "string";
743
+ return (hasOwnProperty(block, "data") &&
744
+ typeof block.data === "string" &&
745
+ isBase64(block.data) &&
746
+ hasOwnProperty(block, "mimeType") &&
747
+ typeof block.mimeType === "string");
782
748
  }
783
749
  if (block.type === "resource_link") {
784
- return hasOwnProperty(block, "uri")
785
- && typeof block.uri === "string"
786
- && isValidUri(block.uri)
787
- && hasOwnProperty(block, "name")
788
- && typeof block.name === "string"
789
- && (!hasOwnProperty(block, "title") || block.title === undefined || typeof block.title === "string")
790
- && (!hasOwnProperty(block, "description") || block.description === undefined || typeof block.description === "string")
791
- && (!hasOwnProperty(block, "mimeType") || block.mimeType === undefined || typeof block.mimeType === "string")
792
- && (!hasOwnProperty(block, "size") || block.size === undefined || typeof block.size === "number");
793
- }
794
- if (block.type !== "resource"
795
- || !hasOwnProperty(block, "resource")
796
- || typeof block.resource !== "object"
797
- || block.resource === null) {
750
+ return (hasOwnProperty(block, "uri") &&
751
+ typeof block.uri === "string" &&
752
+ isValidUri(block.uri) &&
753
+ hasOwnProperty(block, "name") &&
754
+ typeof block.name === "string" &&
755
+ (!hasOwnProperty(block, "title") ||
756
+ block.title === undefined ||
757
+ typeof block.title === "string") &&
758
+ (!hasOwnProperty(block, "description") ||
759
+ block.description === undefined ||
760
+ typeof block.description === "string") &&
761
+ (!hasOwnProperty(block, "mimeType") ||
762
+ block.mimeType === undefined ||
763
+ typeof block.mimeType === "string") &&
764
+ (!hasOwnProperty(block, "size") ||
765
+ block.size === undefined ||
766
+ typeof block.size === "number"));
767
+ }
768
+ if (block.type !== "resource" ||
769
+ !hasOwnProperty(block, "resource") ||
770
+ typeof block.resource !== "object" ||
771
+ block.resource === null) {
798
772
  return false;
799
773
  }
800
774
  return isResourceContents(block.resource);
801
775
  }
802
776
  function isResourceContents(value) {
803
- if (typeof value !== "object"
804
- || value === null
805
- || !hasOwnProperty(value, "uri")
806
- || typeof value.uri !== "string"
807
- || !isValidUri(value.uri)) {
777
+ if (typeof value !== "object" ||
778
+ value === null ||
779
+ !hasOwnProperty(value, "uri") ||
780
+ typeof value.uri !== "string" ||
781
+ !isValidUri(value.uri)) {
808
782
  return false;
809
783
  }
810
- if (hasOwnProperty(value, "mimeType") && value.mimeType !== undefined && typeof value.mimeType !== "string") {
784
+ if (hasOwnProperty(value, "mimeType") &&
785
+ value.mimeType !== undefined &&
786
+ typeof value.mimeType !== "string") {
811
787
  return false;
812
788
  }
813
- return (hasOwnProperty(value, "text") && typeof value.text === "string")
814
- || (hasOwnProperty(value, "blob") && typeof value.blob === "string" && isBase64(value.blob));
789
+ return ((hasOwnProperty(value, "text") && typeof value.text === "string") ||
790
+ (hasOwnProperty(value, "blob") &&
791
+ typeof value.blob === "string" &&
792
+ isBase64(value.blob)));
815
793
  }
816
794
  function hasValidContentAnnotations(value) {
817
- if (!hasOwnProperty(value, "annotations") || value.annotations === undefined) {
795
+ if (!hasOwnProperty(value, "annotations") ||
796
+ value.annotations === undefined) {
818
797
  return true;
819
798
  }
820
799
  if (!isJsonObject(value.annotations)) {
821
800
  return false;
822
801
  }
823
802
  const { audience, priority, lastModified } = value.annotations;
824
- return (audience === undefined || (Array.isArray(audience) && audience.every((item) => item === "user" || item === "assistant")))
825
- && (priority === undefined || typeof priority === "number")
826
- && (lastModified === undefined || typeof lastModified === "string");
803
+ return ((audience === undefined ||
804
+ (Array.isArray(audience) &&
805
+ audience.every((item) => item === "user" || item === "assistant"))) &&
806
+ (priority === undefined || typeof priority === "number") &&
807
+ (lastModified === undefined || typeof lastModified === "string"));
827
808
  }
828
809
  function isBase64(value) {
829
810
  if (value.length === 0) {
@@ -836,7 +817,8 @@ function isBase64(value) {
836
817
  const paddingStart = value.indexOf("=");
837
818
  const encoded = paddingStart === -1 ? value : value.slice(0, paddingStart);
838
819
  const padding = paddingStart === -1 ? "" : value.slice(paddingStart);
839
- if (padding.length > 2 || [...padding].some((character) => character !== "=")) {
820
+ if (padding.length > 2 ||
821
+ [...padding].some((character) => character !== "=")) {
840
822
  return false;
841
823
  }
842
824
  if ([...encoded].some((character) => !alphabet.includes(character))) {
@@ -848,7 +830,10 @@ function isPromptContentItem(value) {
848
830
  if (!isContentItem(value)) {
849
831
  return false;
850
832
  }
851
- return !(typeof value === "object" && value !== null && hasOwnProperty(value, "type") && value.type === "resource_link");
833
+ return !(typeof value === "object" &&
834
+ value !== null &&
835
+ hasOwnProperty(value, "type") &&
836
+ value.type === "resource_link");
852
837
  }
853
838
  function hasOwnProperty(value, name) {
854
839
  return Object.prototype.hasOwnProperty.call(value, name);
package/dist/types.d.ts CHANGED
@@ -164,10 +164,7 @@ export interface ResourceTemplateDefinition extends ResourceTemplate {
164
164
  }
165
165
  export interface HandleResult {
166
166
  result?: unknown;
167
- error?: {
168
- code: number;
169
- message: string;
170
- };
167
+ error?: JSONRPCError;
171
168
  }
172
169
  export type PromptContentItem = {
173
170
  type: "text";
package/dist/types.js CHANGED
@@ -5,7 +5,7 @@ export const JSON_RPC_ERROR_CODES = Object.freeze({
5
5
  METHOD_NOT_FOUND: -32601,
6
6
  INVALID_PARAMS: -32602,
7
7
  INTERNAL_ERROR: -32603,
8
- RESOURCE_NOT_FOUND: -32002
8
+ RESOURCE_NOT_FOUND: -32002,
9
9
  });
10
10
  export class ToolError extends Error {
11
11
  code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-stdio-mcp-server",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "bugs": {
5
5
  "url": "https://github.com/poe-platform/poe-code/issues"
6
6
  },