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