tiny-stdio-mcp-server 0.1.7 → 0.1.9

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,3 +1,4 @@
1
+ import { type FromUrlOptions } from "./remote.js";
1
2
  export interface AudioContent {
2
3
  type: "audio";
3
4
  data: string;
@@ -7,7 +8,7 @@ export declare class Audio {
7
8
  private readonly base64Data;
8
9
  private readonly mimeType;
9
10
  private constructor();
10
- static fromUrl(url: string): Promise<Audio>;
11
+ static fromUrl(url: string, options?: FromUrlOptions): Promise<Audio>;
11
12
  static fromBytes(data: Uint8Array, format?: string): Audio;
12
13
  static fromBase64(base64: string, mimeType: string): Audio;
13
14
  toContentBlock(): AudioContent;
@@ -1,4 +1,5 @@
1
1
  import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
2
+ import { readRemoteBytes } from "./remote.js";
2
3
  const SUPPORTED_AUDIO_MIMES = new Set([
3
4
  "audio/mpeg",
4
5
  "audio/wav",
@@ -19,13 +20,12 @@ export class Audio {
19
20
  this.base64Data = base64Data;
20
21
  this.mimeType = mimeType;
21
22
  }
22
- static async fromUrl(url) {
23
+ static async fromUrl(url, options) {
23
24
  const response = await fetch(url);
24
25
  if (!response.ok) {
25
26
  throw new Error(`Failed to fetch audio from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
26
27
  }
27
- const arrayBuffer = await response.arrayBuffer();
28
- const data = new Uint8Array(arrayBuffer);
28
+ const data = await readRemoteBytes(response, "audio", url, options);
29
29
  const detected = fileTypeFromBuffer(data);
30
30
  let mimeType;
31
31
  if (detected && SUPPORTED_AUDIO_MIMES.has(detected.mime)) {
@@ -1,3 +1,4 @@
1
+ import { type FromUrlOptions } from "./remote.js";
1
2
  export interface TextResourceContents {
2
3
  uri: string;
3
4
  mimeType: string;
@@ -19,7 +20,7 @@ export declare class File {
19
20
  private readonly name?;
20
21
  private readonly charset;
21
22
  private constructor();
22
- static fromUrl(url: string): Promise<File>;
23
+ static fromUrl(url: string, options?: FromUrlOptions): Promise<File>;
23
24
  static fromBytes(data: Uint8Array, mimeType: string): File;
24
25
  static fromText(text: string, mimeType?: string): File;
25
26
  static fromBase64(base64: string, mimeType: string): File;
@@ -1,4 +1,5 @@
1
1
  import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
2
+ import { readRemoteBytes } from "./remote.js";
2
3
  function isTextMimeType(mimeType) {
3
4
  const normalizedMimeType = mimeType.toLowerCase();
4
5
  return (normalizedMimeType.startsWith("text/") ||
@@ -22,13 +23,12 @@ export class File {
22
23
  this.name = name;
23
24
  this.charset = charset;
24
25
  }
25
- static async fromUrl(url) {
26
+ static async fromUrl(url, options) {
26
27
  const response = await fetch(url);
27
28
  if (!response.ok) {
28
29
  throw new Error(`Failed to fetch file from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
29
30
  }
30
- const arrayBuffer = await response.arrayBuffer();
31
- const data = new Uint8Array(arrayBuffer);
31
+ const data = await readRemoteBytes(response, "file", url, options);
32
32
  const detected = fileTypeFromBuffer(data);
33
33
  let mimeType;
34
34
  if (detected) {
@@ -1,3 +1,4 @@
1
+ import { type FromUrlOptions } from "./remote.js";
1
2
  export interface ImageContent {
2
3
  type: "image";
3
4
  data: string;
@@ -7,7 +8,7 @@ export declare class Image {
7
8
  private readonly base64Data;
8
9
  private readonly mimeType;
9
10
  private constructor();
10
- static fromUrl(url: string): Promise<Image>;
11
+ static fromUrl(url: string, options?: FromUrlOptions): Promise<Image>;
11
12
  static fromBytes(data: Uint8Array, format?: string): Image;
12
13
  static fromBase64(base64: string, mimeType: string): Image;
13
14
  toContentBlock(): ImageContent;
@@ -1,4 +1,5 @@
1
1
  import { assertBase64, fileTypeFromBuffer, parseContentType, safeRemoteLabel } from "./mime.js";
2
+ import { readRemoteBytes } from "./remote.js";
2
3
  const SUPPORTED_IMAGE_MIMES = new Set([
3
4
  "image/png",
4
5
  "image/jpeg",
@@ -12,13 +13,12 @@ export class Image {
12
13
  this.base64Data = base64Data;
13
14
  this.mimeType = mimeType;
14
15
  }
15
- static async fromUrl(url) {
16
+ static async fromUrl(url, options) {
16
17
  const response = await fetch(url);
17
18
  if (!response.ok) {
18
19
  throw new Error(`Failed to fetch image from ${safeRemoteLabel(url)}: ${response.status} ${response.statusText}`);
19
20
  }
20
- const arrayBuffer = await response.arrayBuffer();
21
- const data = new Uint8Array(arrayBuffer);
21
+ const data = await readRemoteBytes(response, "image", url, options);
22
22
  const detected = fileTypeFromBuffer(data);
23
23
  let mimeType;
24
24
  if (detected && SUPPORTED_IMAGE_MIMES.has(detected.mime)) {
@@ -1,4 +1,6 @@
1
1
  export { fileTypeFromBuffer, type FileTypeResult } from "./mime.js";
2
+ export { DEFAULT_FROM_URL_MAX_BYTES } from "./remote.js";
3
+ export type { FromUrlOptions } from "./remote.js";
2
4
  export { Image, type ImageContent } from "./image.js";
3
5
  export { Audio, type AudioContent } from "./audio.js";
4
6
  export { File, type EmbeddedResource, type TextResourceContents, type BlobResourceContents, } from "./file.js";
@@ -1,5 +1,6 @@
1
1
  // MIME detection
2
2
  export { fileTypeFromBuffer } from "./mime.js";
3
+ export { DEFAULT_FROM_URL_MAX_BYTES } from "./remote.js";
3
4
  // Content helpers
4
5
  export { Image } from "./image.js";
5
6
  export { Audio } from "./audio.js";
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_FROM_URL_MAX_BYTES: number;
2
+ export interface FromUrlOptions {
3
+ maxBytes?: number;
4
+ }
5
+ export declare function readRemoteBytes(response: Response, kind: string, url: string, options?: FromUrlOptions): Promise<Uint8Array>;
@@ -0,0 +1,69 @@
1
+ import { safeRemoteLabel } from "./mime.js";
2
+ export const DEFAULT_FROM_URL_MAX_BYTES = 5 * 1024 * 1024;
3
+ function resolveMaxBytes(options) {
4
+ const maxBytes = options?.maxBytes ?? DEFAULT_FROM_URL_MAX_BYTES;
5
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
6
+ throw new Error("maxBytes must be a positive integer");
7
+ }
8
+ return maxBytes;
9
+ }
10
+ function createSizeError(kind, url, maxBytes) {
11
+ return new Error(`Remote ${kind} from ${safeRemoteLabel(url)} exceeds maximum size of ${maxBytes} bytes`);
12
+ }
13
+ function readContentLength(response) {
14
+ const rawContentLength = response.headers.get("content-length");
15
+ if (typeof rawContentLength !== "string") {
16
+ return undefined;
17
+ }
18
+ const contentLength = Number(rawContentLength.trim());
19
+ if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
20
+ return undefined;
21
+ }
22
+ return contentLength;
23
+ }
24
+ function combineChunks(chunks, totalBytes) {
25
+ const combined = new Uint8Array(totalBytes);
26
+ let offset = 0;
27
+ for (const chunk of chunks) {
28
+ combined.set(chunk, offset);
29
+ offset += chunk.byteLength;
30
+ }
31
+ return combined;
32
+ }
33
+ async function readStreamBytes(body, kind, url, maxBytes) {
34
+ const reader = body.getReader();
35
+ const chunks = [];
36
+ let totalBytes = 0;
37
+ try {
38
+ while (true) {
39
+ const { done, value } = await reader.read();
40
+ if (done) {
41
+ return combineChunks(chunks, totalBytes);
42
+ }
43
+ totalBytes += value.byteLength;
44
+ if (totalBytes > maxBytes) {
45
+ await reader.cancel().catch(() => undefined);
46
+ throw createSizeError(kind, url, maxBytes);
47
+ }
48
+ chunks.push(value);
49
+ }
50
+ }
51
+ finally {
52
+ reader.releaseLock();
53
+ }
54
+ }
55
+ export async function readRemoteBytes(response, kind, url, options) {
56
+ const maxBytes = resolveMaxBytes(options);
57
+ const contentLength = readContentLength(response);
58
+ if (contentLength !== undefined && contentLength > maxBytes) {
59
+ throw createSizeError(kind, url, maxBytes);
60
+ }
61
+ if (response.body) {
62
+ return readStreamBytes(response.body, kind, url, maxBytes);
63
+ }
64
+ const arrayBuffer = await response.arrayBuffer();
65
+ if (arrayBuffer.byteLength > maxBytes) {
66
+ throw createSizeError(kind, url, maxBytes);
67
+ }
68
+ return new Uint8Array(arrayBuffer);
69
+ }
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { createServer } from "./server.js";
2
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
- export { Image, Audio, File, toContentBlocks, fileTypeFromBuffer, } from "./content/index.js";
6
- export type { ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, } from "./content/index.js";
5
+ export { Image, Audio, File, toContentBlocks, fileTypeFromBuffer, DEFAULT_FROM_URL_MAX_BYTES, } from "./content/index.js";
6
+ export type { ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, FromUrlOptions, } from "./content/index.js";
7
7
  export type { ToolReturn } from "./content/index.js";
8
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/index.js CHANGED
@@ -3,5 +3,5 @@ export { createServer } from "./server.js";
3
3
  // Schema
4
4
  export { defineSchema } from "./schema.js";
5
5
  // Content helpers
6
- export { Image, Audio, File, toContentBlocks, fileTypeFromBuffer, } from "./content/index.js";
6
+ export { Image, Audio, File, toContentBlocks, fileTypeFromBuffer, DEFAULT_FROM_URL_MAX_BYTES, } from "./content/index.js";
7
7
  export { JSON_RPC_ERROR_CODES, ToolError } from "./types.js";
package/dist/schema.d.ts CHANGED
@@ -4,6 +4,7 @@ interface SchemaPropertyDef {
4
4
  type: SchemaPropertyType;
5
5
  description?: string;
6
6
  optional?: boolean;
7
+ [keyword: string]: unknown;
7
8
  }
8
9
  type SchemaDefinition = Record<string, SchemaPropertyDef>;
9
10
  type InferType<T extends SchemaPropertyType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : T extends "object" ? Record<string, unknown> : T extends "array" ? unknown[] : never;
package/dist/schema.js CHANGED
@@ -2,14 +2,17 @@ export function defineSchema(definition) {
2
2
  const properties = {};
3
3
  const required = [];
4
4
  for (const [key, prop] of Object.entries(definition)) {
5
+ const jsonSchemaProperty = {};
6
+ for (const [propertyKey, propertyValue] of Object.entries(prop)) {
7
+ if (propertyKey !== "optional") {
8
+ jsonSchemaProperty[propertyKey] = propertyValue;
9
+ }
10
+ }
5
11
  Object.defineProperty(properties, key, {
6
12
  enumerable: true,
7
13
  configurable: true,
8
14
  writable: true,
9
- value: {
10
- type: prop.type,
11
- ...(prop.description !== undefined && { description: prop.description }),
12
- },
15
+ value: jsonSchemaProperty,
13
16
  });
14
17
  if (!prop.optional) {
15
18
  required.push(key);
package/dist/server.js CHANGED
@@ -320,6 +320,7 @@ export function createServer(options) {
320
320
  };
321
321
  const server = {
322
322
  tool(name, description, inputSchema, handler, outputSchema) {
323
+ assertNonEmptyName(name, "Tool name required");
323
324
  if (outputSchema !== undefined) {
324
325
  assertSupportedOutputSchema(outputSchema);
325
326
  }
@@ -333,6 +334,7 @@ export function createServer(options) {
333
334
  return server;
334
335
  },
335
336
  registerTool(definition, handler) {
337
+ assertNonEmptyName(definition.name, "Tool name required");
336
338
  if (definition.outputSchema !== undefined) {
337
339
  assertSupportedOutputSchema(definition.outputSchema);
338
340
  }
@@ -343,6 +345,7 @@ export function createServer(options) {
343
345
  return server;
344
346
  },
345
347
  prompt(definition, handler) {
348
+ assertNonEmptyName(definition.name, "Prompt name required");
346
349
  prompts.set(definition.name, { ...definition, handler });
347
350
  return server;
348
351
  },
@@ -354,7 +357,7 @@ export function createServer(options) {
354
357
  return server;
355
358
  },
356
359
  resourceTemplate(definition, handler) {
357
- uriTemplateParser.parse(definition.uriTemplate);
360
+ assertReadableUriTemplate(definition.uriTemplate);
358
361
  new UriTemplate(definition.uriTemplate);
359
362
  resourceTemplates.set(definition.uriTemplate, { ...definition, handler });
360
363
  return server;
@@ -536,6 +539,20 @@ function isValidUri(uri) {
536
539
  return false;
537
540
  }
538
541
  }
542
+ function assertNonEmptyName(name, message) {
543
+ if (name.length === 0) {
544
+ throw new Error(message);
545
+ }
546
+ }
547
+ function assertReadableUriTemplate(uriTemplate) {
548
+ const parsed = uriTemplateParser.parse(uriTemplate);
549
+ const expanded = parsed.expand(new Proxy({}, {
550
+ get: (_target, property) => typeof property === "string" ? "value" : undefined,
551
+ }));
552
+ if (typeof expanded !== "string" || !isValidUri(expanded)) {
553
+ throw new Error(`Invalid resource URI template: ${uriTemplate}`);
554
+ }
555
+ }
539
556
  function toStringArguments(value) {
540
557
  if (value === undefined) {
541
558
  return {};
@@ -588,9 +605,13 @@ function normalizeToolResult(handlerResult, outputSchema) {
588
605
  throw new Error("Invalid tool result");
589
606
  }
590
607
  if (outputSchema === undefined) {
591
- return isCallToolResult(handlerResult)
608
+ const result = isCallToolResult(handlerResult)
592
609
  ? handlerResult
593
610
  : { content: toContentBlocks(handlerResult) };
611
+ if (!isCallToolResult(result)) {
612
+ throw new Error("Invalid tool result");
613
+ }
614
+ return result;
594
615
  }
595
616
  const structuredContent = isCallToolResult(handlerResult)
596
617
  ? handlerResult.structuredContent
@@ -695,7 +716,8 @@ function isGetPromptResult(value) {
695
716
  if (typeof value !== "object" || value === null || !hasOwnProperty(value, "messages")) {
696
717
  return false;
697
718
  }
698
- return Array.isArray(value.messages)
719
+ return (!hasOwnProperty(value, "description") || value.description === undefined || typeof value.description === "string")
720
+ && Array.isArray(value.messages)
699
721
  && value.messages.every((message) => typeof message === "object"
700
722
  && message !== null
701
723
  && hasOwnProperty(message, "role")
@@ -708,13 +730,7 @@ function isReadResourceResult(value) {
708
730
  return false;
709
731
  }
710
732
  return Array.isArray(value.contents)
711
- && value.contents.every((content) => typeof content === "object"
712
- && content !== null
713
- && hasOwnProperty(content, "uri")
714
- && typeof content.uri === "string"
715
- && isValidUri(content.uri)
716
- && ((hasOwnProperty(content, "text") && typeof content.text === "string")
717
- || (hasOwnProperty(content, "blob") && typeof content.blob === "string" && isBase64(content.blob))));
733
+ && value.contents.every(isResourceContents);
718
734
  }
719
735
  function hasContentArray(value) {
720
736
  return typeof value === "object" && value !== null && hasOwnProperty(value, "content")
@@ -725,6 +741,9 @@ function isContentItem(value) {
725
741
  return false;
726
742
  }
727
743
  const block = value;
744
+ if (!hasValidContentAnnotations(block)) {
745
+ return false;
746
+ }
728
747
  if (block.type === "text") {
729
748
  return hasOwnProperty(block, "text") && typeof block.text === "string";
730
749
  }
@@ -738,8 +757,13 @@ function isContentItem(value) {
738
757
  if (block.type === "resource_link") {
739
758
  return hasOwnProperty(block, "uri")
740
759
  && typeof block.uri === "string"
760
+ && isValidUri(block.uri)
741
761
  && hasOwnProperty(block, "name")
742
- && typeof block.name === "string";
762
+ && typeof block.name === "string"
763
+ && (!hasOwnProperty(block, "title") || block.title === undefined || typeof block.title === "string")
764
+ && (!hasOwnProperty(block, "description") || block.description === undefined || typeof block.description === "string")
765
+ && (!hasOwnProperty(block, "mimeType") || block.mimeType === undefined || typeof block.mimeType === "string")
766
+ && (!hasOwnProperty(block, "size") || block.size === undefined || typeof block.size === "number");
743
767
  }
744
768
  if (block.type !== "resource"
745
769
  || !hasOwnProperty(block, "resource")
@@ -747,12 +771,33 @@ function isContentItem(value) {
747
771
  || block.resource === null) {
748
772
  return false;
749
773
  }
750
- const resource = block.resource;
751
- return hasOwnProperty(resource, "uri")
752
- && typeof resource.uri === "string"
753
- && (!hasOwnProperty(resource, "mimeType") || typeof resource.mimeType === "string")
754
- && ((hasOwnProperty(resource, "text") && typeof resource.text === "string")
755
- || (hasOwnProperty(resource, "blob") && typeof resource.blob === "string" && isBase64(resource.blob)));
774
+ return isResourceContents(block.resource);
775
+ }
776
+ function isResourceContents(value) {
777
+ if (typeof value !== "object"
778
+ || value === null
779
+ || !hasOwnProperty(value, "uri")
780
+ || typeof value.uri !== "string"
781
+ || !isValidUri(value.uri)) {
782
+ return false;
783
+ }
784
+ if (hasOwnProperty(value, "mimeType") && value.mimeType !== undefined && typeof value.mimeType !== "string") {
785
+ return false;
786
+ }
787
+ return (hasOwnProperty(value, "text") && typeof value.text === "string")
788
+ || (hasOwnProperty(value, "blob") && typeof value.blob === "string" && isBase64(value.blob));
789
+ }
790
+ function hasValidContentAnnotations(value) {
791
+ if (!hasOwnProperty(value, "annotations") || value.annotations === undefined) {
792
+ return true;
793
+ }
794
+ if (!isJsonObject(value.annotations)) {
795
+ return false;
796
+ }
797
+ const { audience, priority, lastModified } = value.annotations;
798
+ return (audience === undefined || (Array.isArray(audience) && audience.every((item) => item === "user" || item === "assistant")))
799
+ && (priority === undefined || typeof priority === "number")
800
+ && (lastModified === undefined || typeof lastModified === "string");
756
801
  }
757
802
  function isBase64(value) {
758
803
  if (value.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-stdio-mcp-server",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "bugs": {
5
5
  "url": "https://github.com/poe-platform/poe-code/issues"
6
6
  },