tiny-stdio-mcp-server 0.1.5 → 0.1.7

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
@@ -42,9 +42,9 @@ Creates a new MCP server.
42
42
  const server = createServer({ name: "my-server", version: "1.0.0" });
43
43
  ```
44
44
 
45
- ### `.tool(name, description, schema, handler)`
45
+ ### `.tool(name, description, schema, handler, outputSchema?)`
46
46
 
47
- Register a tool. The handler receives typed args matching the schema and returns a string, content helper, or array of either.
47
+ Register a tool. The handler receives typed args matching the schema and returns a string, content helper, array of content, or a typed object when `outputSchema` is supplied.
48
48
 
49
49
  ```ts
50
50
  const schema = defineSchema({
@@ -58,6 +58,33 @@ 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.
62
+
63
+ ```ts
64
+ const input = defineSchema({
65
+ query: { type: "string" },
66
+ });
67
+ const output = defineSchema({
68
+ items: {
69
+ type: "array",
70
+ items: {
71
+ type: "object",
72
+ properties: {
73
+ title: { type: "string" },
74
+ score: { type: "number" },
75
+ },
76
+ required: ["title", "score"],
77
+ },
78
+ },
79
+ });
80
+
81
+ server.tool("search", "Search", input, async ({ query }) => ({
82
+ items: [{ title: query, score: 1 }],
83
+ }), output);
84
+ ```
85
+
86
+ 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.
87
+
61
88
  ### `.listen()`
62
89
 
63
90
  Start listening on stdin/stdout (standard MCP stdio transport).
package/dist/server.d.ts CHANGED
@@ -1,8 +1,8 @@
1
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
- 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;
4
+ tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: ToolHandler<TIn, TOut>, outputSchema?: TypedSchema<TOut>): Server;
5
+ registerTool<TIn, TOut = never>(definition: Omit<ToolDefinition<TIn, TOut>, "handler">, handler: ToolHandler<TIn, TOut>): Server;
6
6
  prompt(definition: Prompt, handler: PromptHandler): Server;
7
7
  resource(definition: Resource, handler: ResourceHandler): Server;
8
8
  resourceTemplate(definition: ResourceTemplate, handler: ResourceHandler): Server;
package/dist/server.js CHANGED
@@ -131,16 +131,9 @@ export function createServer(options) {
131
131
  }
132
132
  try {
133
133
  const handlerResult = await tool.handler(toolArgs);
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");
134
+ 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");
144
137
  }
145
138
  return { result };
146
139
  }
@@ -326,16 +319,23 @@ export function createServer(options) {
326
319
  }));
327
320
  };
328
321
  const server = {
329
- tool(name, description, inputSchema, handler) {
322
+ tool(name, description, inputSchema, handler, outputSchema) {
323
+ if (outputSchema !== undefined) {
324
+ assertSupportedOutputSchema(outputSchema);
325
+ }
330
326
  tools.set(name, {
331
327
  name,
332
328
  description,
333
329
  inputSchema: inputSchema,
330
+ ...(outputSchema === undefined ? {} : { outputSchema: outputSchema }),
334
331
  handler: handler,
335
332
  });
336
333
  return server;
337
334
  },
338
335
  registerTool(definition, handler) {
336
+ if (definition.outputSchema !== undefined) {
337
+ assertSupportedOutputSchema(definition.outputSchema);
338
+ }
339
339
  tools.set(definition.name, {
340
340
  ...definition,
341
341
  handler: handler,
@@ -571,7 +571,125 @@ function matchesUriTemplate(template, uri) {
571
571
  }
572
572
  }
573
573
  function isCallToolResult(value) {
574
- return hasContentArray(value) && value.content.every(isContentItem);
574
+ if (!hasContentArray(value) || !value.content.every(isContentItem)) {
575
+ return false;
576
+ }
577
+ if (hasOwnProperty(value, "structuredContent")
578
+ && value.structuredContent !== undefined
579
+ && !isJsonObject(value.structuredContent)) {
580
+ return false;
581
+ }
582
+ return !(hasOwnProperty(value, "isError")
583
+ && value.isError !== undefined
584
+ && typeof value.isError !== "boolean");
585
+ }
586
+ function normalizeToolResult(handlerResult, outputSchema) {
587
+ if (hasContentArray(handlerResult) && !isCallToolResult(handlerResult)) {
588
+ throw new Error("Invalid tool result");
589
+ }
590
+ if (outputSchema === undefined) {
591
+ return isCallToolResult(handlerResult)
592
+ ? handlerResult
593
+ : { content: toContentBlocks(handlerResult) };
594
+ }
595
+ const structuredContent = isCallToolResult(handlerResult)
596
+ ? handlerResult.structuredContent
597
+ : handlerResult;
598
+ if (!isJsonObject(structuredContent)) {
599
+ throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, "Structured tool result must be an object");
600
+ }
601
+ return {
602
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
603
+ ...(isCallToolResult(handlerResult) && handlerResult.isError !== undefined
604
+ ? { isError: handlerResult.isError }
605
+ : {}),
606
+ structuredContent,
607
+ };
608
+ }
609
+ function assertSupportedOutputSchema(schema) {
610
+ assertObjectRootSchema(schema, "outputSchema");
611
+ assertSupportedJsonSchema(schema, "outputSchema");
612
+ }
613
+ function assertObjectRootSchema(schema, path) {
614
+ if (schema.type !== "object") {
615
+ throw new Error(`${path} root type must be "object"`);
616
+ }
617
+ }
618
+ function assertSupportedJsonSchema(schema, path) {
619
+ for (const keyword of ["anyOf", "allOf", "not", "if", "then", "else", "contains", "prefixItems"]) {
620
+ if (schema[keyword] !== undefined) {
621
+ throw new Error(`${path} uses unsupported keyword "${keyword}"`);
622
+ }
623
+ }
624
+ const type = schema.type;
625
+ if (Array.isArray(type)) {
626
+ const supported = new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
627
+ for (const item of type) {
628
+ if (!supported.has(item)) {
629
+ throw new Error(`${path} uses unsupported type "${item}"`);
630
+ }
631
+ }
632
+ }
633
+ else if (type !== undefined &&
634
+ type !== "string" &&
635
+ type !== "number" &&
636
+ type !== "integer" &&
637
+ type !== "boolean" &&
638
+ type !== "object" &&
639
+ type !== "array") {
640
+ throw new Error(`${path} uses unsupported type "${type}"`);
641
+ }
642
+ if (isJsonObject(schema.properties)) {
643
+ for (const [key, child] of Object.entries(schema.properties)) {
644
+ if (isJsonObject(child)) {
645
+ assertSupportedJsonSchema(child, `${path}.properties.${key}`);
646
+ }
647
+ else {
648
+ throw new Error(`${path}.properties.${key} must be an object schema`);
649
+ }
650
+ }
651
+ }
652
+ else if (schema.properties !== undefined) {
653
+ throw new Error(`${path}.properties must be an object`);
654
+ }
655
+ const additionalProperties = schema.additionalProperties;
656
+ if (typeof additionalProperties === "object" && additionalProperties !== null) {
657
+ if (Array.isArray(additionalProperties)) {
658
+ throw new Error(`${path}.additionalProperties must be an object schema or boolean`);
659
+ }
660
+ assertSupportedJsonSchema(additionalProperties, `${path}.additionalProperties`);
661
+ }
662
+ else if (additionalProperties !== undefined
663
+ && typeof additionalProperties !== "boolean") {
664
+ throw new Error(`${path}.additionalProperties must be an object schema or boolean`);
665
+ }
666
+ const items = schema.items;
667
+ if (Array.isArray(items)) {
668
+ throw new Error(`${path}.items uses unsupported tuple array schemas`);
669
+ }
670
+ if (typeof items === "object" && items !== null && !Array.isArray(items)) {
671
+ assertSupportedJsonSchema(items, `${path}.items`);
672
+ }
673
+ else if (items !== undefined && typeof items !== "boolean") {
674
+ throw new Error(`${path}.items must be an object schema or boolean`);
675
+ }
676
+ const oneOf = schema.oneOf;
677
+ if (Array.isArray(oneOf)) {
678
+ for (const [index, child] of oneOf.entries()) {
679
+ if (typeof child === "object" && child !== null && !Array.isArray(child)) {
680
+ assertSupportedJsonSchema(child, `${path}.oneOf[${index}]`);
681
+ }
682
+ else {
683
+ throw new Error(`${path}.oneOf[${index}] must be an object schema`);
684
+ }
685
+ }
686
+ }
687
+ else if (oneOf !== undefined) {
688
+ throw new Error(`${path}.oneOf must be an array`);
689
+ }
690
+ }
691
+ function isJsonObject(value) {
692
+ return typeof value === "object" && value !== null && !Array.isArray(value);
575
693
  }
576
694
  function isGetPromptResult(value) {
577
695
  if (typeof value !== "object" || value === null || !hasOwnProperty(value, "messages")) {
package/dist/types.d.ts CHANGED
@@ -215,8 +215,8 @@ export interface ServerOptions {
215
215
  supportResourceSubscriptions?: boolean;
216
216
  }
217
217
  import type { ToolReturn } from "./content/index.js";
218
- export type ToolHandler<T = Record<string, unknown>> = (args: T) => Promise<ToolReturn | CallToolResult> | ToolReturn | CallToolResult;
219
- export interface ToolDefinition<T = Record<string, unknown>> {
218
+ export type ToolHandler<T = Record<string, unknown>, TOut = ToolReturn> = (args: T) => Promise<TOut | CallToolResult> | TOut | CallToolResult;
219
+ export interface ToolDefinition<T = Record<string, unknown>, TOut = ToolReturn> {
220
220
  name: string;
221
221
  title?: string;
222
222
  description?: string;
@@ -226,7 +226,7 @@ export interface ToolDefinition<T = Record<string, unknown>> {
226
226
  execution?: ToolExecution;
227
227
  icons?: Icon[];
228
228
  _meta?: Record<string, unknown>;
229
- handler: ToolHandler<T>;
229
+ handler: ToolHandler<T, TOut>;
230
230
  }
231
231
  export interface Transport {
232
232
  readable: NodeJS.ReadableStream;
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "tiny-stdio-mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
+ "bugs": {
5
+ "url": "https://github.com/poe-platform/poe-code/issues"
6
+ },
4
7
  "description": "Minimal MCP server over stdio with typed tools and rich content helpers",
5
8
  "type": "module",
6
9
  "main": "dist/index.js",