Merge branch 'main' into ihrpr/http-streamable-server

This commit is contained in:
ihrpr 2025-04-07 14:29:09 +01:00
commit 251bb9d97d
No known key found for this signature in database
22 changed files with 133 additions and 464 deletions

View file

@ -46,7 +46,6 @@ jobs:
- run: npm ci
# TODO: Add --provenance once the repo is public
- run: npm publish --access public
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -1,8 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"trailingComma": "all",
"jsxBracketSameLine": true,
"semi": true,
"singleQuote": false
}

View file

@ -455,13 +455,6 @@ const client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
@ -471,15 +464,20 @@ await client.connect(transport);
const prompts = await client.listPrompts();
// Get a prompt
const prompt = await client.getPrompt("example-prompt", {
arg1: "value"
const prompt = await client.getPrompt({
name: "example-prompt",
arguments: {
arg1: "value"
}
});
// List resources
const resources = await client.listResources();
// Read a resource
const resource = await client.readResource("file:///example.txt");
const resource = await client.readResource({
uri: "file:///example.txt"
});
// Call a tool
const result = await client.callTool({

View file

@ -12,6 +12,5 @@ export default {
transformIgnorePatterns: [
"/node_modules/(?!eventsource)/"
],
collectCoverageFrom: ["src/**/*.ts"],
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
};

51
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/sdk",
"version": "1.8.0",
"version": "1.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/sdk",
"version": "1.8.0",
"version": "1.9.0",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
@ -60,10 +60,11 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz",
"integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==",
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
@ -211,25 +212,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.0"
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz",
"integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.0"
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -461,14 +464,15 @@
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
@ -502,10 +506,11 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"

View file

@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/sdk",
"version": "1.8.0",
"version": "1.9.0",
"description": "Model Context Protocol implementation for TypeScript",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@ -41,7 +41,6 @@
"prepack": "npm run build:esm && npm run build:cjs",
"lint": "eslint src/",
"test": "jest",
"coverage": "jest --coverage",
"start": "npm run server",
"server": "tsx watch --clear-screen=false src/cli.ts server",
"client": "tsx src/cli.ts client"

View file

@ -230,10 +230,19 @@ test("should respect server capabilities", async () => {
await expect(client.listResources()).resolves.not.toThrow();
await expect(client.listTools()).resolves.not.toThrow();
// This should throw because prompts are not supported
// These should throw because prompts, logging, and completions are not supported
await expect(client.listPrompts()).rejects.toThrow(
"Server does not support prompts",
);
await expect(client.setLoggingLevel("error")).rejects.toThrow(
"Server does not support logging",
);
await expect(
client.complete({
ref: { type: "ref/prompt", name: "test" },
argument: { name: "test", value: "test" },
}),
).rejects.toThrow("Server does not support completions");
});
test("should respect client notification capabilities", async () => {

View file

@ -124,7 +124,7 @@ export class Client<
}
}
override async connect(transport: Transport): Promise<void> {
override async connect(transport: Transport, options?: RequestOptions): Promise<void> {
await super.connect(transport);
try {
@ -138,6 +138,7 @@ export class Client<
},
},
InitializeResultSchema,
options
);
if (result === undefined) {
@ -237,9 +238,9 @@ export class Client<
break;
case "completion/complete":
if (!this._serverCapabilities?.prompts) {
if (!this._serverCapabilities?.completions) {
throw new Error(
`Server does not support prompts (required for ${method})`,
`Server does not support completions (required for ${method})`,
);
}
break;

View file

@ -24,7 +24,7 @@ describe("SSEClientTransport", () => {
// Send SSE headers
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
@ -56,6 +56,8 @@ describe("SSEClientTransport", () => {
baseUrl = new URL(`http://127.0.0.1:${addr.port}`);
done();
});
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(async () => {
@ -183,7 +185,7 @@ describe("SSEClientTransport", () => {
if (req.method === "GET") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
res.write("event: endpoint\n");
@ -397,7 +399,7 @@ describe("SSEClientTransport", () => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
res.write("event: endpoint\n");
@ -524,7 +526,7 @@ describe("SSEClientTransport", () => {
if (auth === "Bearer new-token") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
res.write("event: endpoint\n");
@ -610,7 +612,7 @@ describe("SSEClientTransport", () => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
res.write("event: endpoint\n");

View file

@ -12,7 +12,6 @@ export class InMemoryTransport implements Transport {
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
sessionId?: string;
user?: unknown;
/**
* Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a Client and one to a Server.

View file

@ -31,6 +31,10 @@ describe("requireBearerAuth middleware", () => {
set: jest.fn().mockReturnThis(),
};
nextFunction = jest.fn();
jest.spyOn(console, 'error').mockImplementation(() => {});
})
afterEach(() => {
jest.clearAllMocks();
});

View file

@ -259,6 +259,11 @@ describe('MCP Auth Router', () => {
issuerUrl: new URL('https://auth.example.com')
};
app.use(mcpAuthRouter(options));
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('routes to authorization endpoint', async () => {

View file

@ -1,8 +1,7 @@
import { McpServer, ToolCallback } from "./mcp.js";
import { McpServer } from "./mcp.js";
import { Client } from "../client/index.js";
import { InMemoryTransport } from "../inMemory.js";
import { z, ZodRawShape } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { z } from "zod";
import {
ListToolsResultSchema,
CallToolResultSchema,
@ -13,18 +12,10 @@ import {
GetPromptResultSchema,
CompleteResultSchema,
LoggingMessageNotificationSchema,
CallToolRequestSchema,
CallToolRequest,
ListToolsRequestSchema,
ListToolsResult,
Tool,
ServerRequest,
ServerNotification,
} from "../types.js";
import { ResourceTemplate } from "./mcp.js";
import { completable } from "./completable.js";
import { UriTemplate } from "../shared/uriTemplate.js";
import { RequestHandlerExtra } from "../shared/protocol.js";
describe("McpServer", () => {
test("should expose underlying Server instance", () => {
@ -366,8 +357,7 @@ describe("tool()", () => {
};
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
// Set a test sessionId on the server transport
serverTransport.sessionId = "test-session-123";
@ -1420,17 +1410,19 @@ describe("prompt()", () => {
});
// This should succeed
mcpServer.prompt("echo", { message: z.string() }, ({ message }) => ({
messages: [
{
mcpServer.prompt(
"echo",
{ message: z.string() },
({ message }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please process this message: ${message}`,
},
},
],
}));
text: `Please process this message: ${message}`
}
}]
})
);
});
test("should allow registering both resources and prompts with completion handlers", () => {
@ -1463,16 +1455,14 @@ describe("prompt()", () => {
"echo",
{ message: completable(z.string(), () => ["hello", "world"]) },
({ message }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please process this message: ${message}`,
},
},
],
}),
messages: [{
role: "user",
content: {
type: "text",
text: `Please process this message: ${message}`
}
}]
})
);
});
@ -1659,351 +1649,3 @@ describe("prompt()", () => {
expect(result.completion.total).toBe(1);
});
});
describe("McpServer with Auth Extension", () => {
type SessionUser = {
role: string;
[key: string]: unknown;
};
type AccessPolicy = {
allow?: {
roles?: string[];
};
deny?: {
roles?: string[];
};
};
type RegisteredToolWithAuth = {
description?: string;
inputSchema?: z.ZodObject<ZodRawShape>;
callback: ToolCallback<ZodRawShape | undefined>;
accessPolicy?: AccessPolicy;
};
// Just a simple extension to McpServer that adds support for access policies on tools
class McpServerWithAuth extends McpServer {
protected override _registeredTools: {
[name: string]: RegisteredToolWithAuth;
} = {};
checkPermissions(user?: SessionUser, policy?: AccessPolicy): boolean {
if (!policy) {
return true;
}
if (!user) {
return false;
}
// Check deny rules first
if (policy.deny) {
// Check denied roles
if (policy.deny.roles?.includes(user.role)) {
return false;
}
}
// Check allow rules
if (policy.allow) {
let isAllowed = false;
// If no allow rules are specified, default to allowed
if (!policy.allow.roles) {
isAllowed = true;
} else {
// Check allowed roles
if (policy.allow.roles?.includes(user.role)) {
isAllowed = true;
}
}
return isAllowed;
}
// If no rules specified, default to allowed
return true;
}
override tool(
name: string,
cb: ToolCallback,
accessPolicy?: AccessPolicy,
): void;
override tool(
name: string,
description: string,
cb: ToolCallback,
accessPolicy?: AccessPolicy,
): void;
override tool<Args extends ZodRawShape>(
name: string,
paramsSchema: Args,
cb: ToolCallback<Args>,
accessPolicy?: AccessPolicy,
): void;
override tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchema: Args,
cb: ToolCallback<Args>,
accessPolicy?: AccessPolicy,
): void;
override tool(name: string, ...rest: unknown[]): void {
let description: string | undefined;
let paramsSchema: ZodRawShape | undefined;
let accessPolicy: AccessPolicy | undefined;
let cb: ToolCallback<ZodRawShape | undefined>;
// Parse arguments based on their types
if (typeof rest[0] === "function") {
// Case: tool(name, cb, accessPolicy?)
cb = rest[0] as ToolCallback<ZodRawShape | undefined>;
accessPolicy = rest[1] as AccessPolicy | undefined;
} else if (typeof rest[0] === "string") {
// Cases with description
description = rest[0];
if (typeof rest[1] === "function") {
// Case: tool(name, description, cb, accessPolicy?)
cb = rest[1] as ToolCallback<ZodRawShape | undefined>;
accessPolicy = rest[2] as AccessPolicy | undefined;
} else {
// Case: tool(name, description, paramsSchema, cb, accessPolicy?)
paramsSchema = rest[1] as ZodRawShape;
cb = rest[2] as ToolCallback<ZodRawShape>;
accessPolicy = rest[3] as AccessPolicy | undefined;
}
} else {
// Case: tool(name, paramsSchema, cb, accessPolicy?)
paramsSchema = rest[0] as ZodRawShape;
cb = rest[1] as ToolCallback<ZodRawShape>;
accessPolicy = rest[2] as AccessPolicy | undefined;
}
// Register with base class
const args: unknown[] = [name];
if (description) args.push(description);
if (paramsSchema) args.push(paramsSchema);
args.push(cb);
// Set up request handlers if not already initialized
if (!this._toolHandlersInitialized) {
this.server.assertCanSetRequestHandler(
CallToolRequestSchema.shape.method.value,
);
this.server.assertCanSetRequestHandler(
ListToolsRequestSchema.shape.method.value,
);
this.server.registerCapabilities({ tools: {} });
// Add ListToolsRequestSchema handler
this.server.setRequestHandler(
ListToolsRequestSchema,
(request, extra): ListToolsResult => {
const user = extra.user as SessionUser | undefined;
// Filter tools based on permissions
const accessibleTools = Object.entries(this._registeredTools)
.filter(([_, tool]) =>
this.checkPermissions(user, tool.accessPolicy),
)
.map(
([name, tool]): Tool => ({
name,
description: tool.description,
inputSchema: tool.inputSchema
? (zodToJsonSchema(tool.inputSchema, {
strictUnions: true,
}) as Tool["inputSchema"])
: { type: "object" },
}),
);
return { tools: accessibleTools };
},
);
this.server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
const tool = this._registeredTools[request.params.name];
if (!tool) {
throw new Error(`Tool ${request.params.name} not found`);
}
if (
!this.checkPermissions(
extra.user as SessionUser,
tool.accessPolicy,
)
) {
throw new Error(`Access denied for tool: ${request.params.name}`);
}
if (tool.inputSchema) {
const parseResult = await tool.inputSchema.safeParseAsync(
request.params.arguments,
);
if (!parseResult.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`,
);
}
const args = parseResult.data;
const cb = tool.callback as ToolCallback<ZodRawShape>;
return await Promise.resolve(cb(args, extra));
} else {
const cb = tool.callback as ToolCallback<undefined>;
return await Promise.resolve(cb(extra));
}
},
);
this._toolHandlersInitialized = true;
}
McpServer.prototype.tool.apply(
this,
args as Parameters<typeof McpServer.prototype.tool>,
);
this._registeredTools[name].accessPolicy = accessPolicy;
}
}
const mcpServer = new McpServerWithAuth({
name: "test server with auth",
version: "1.0",
});
const client = new Client({
name: "test client",
version: "1.0",
});
mcpServer.tool("public-tool", async () => ({
content: [
{
type: "text",
text: "Public tool response",
},
],
}));
mcpServer.tool(
"protected-tool",
async () => ({
content: [
{
type: "text",
text: "Protected tool response",
},
],
}),
{
allow: {
roles: ["admin"],
},
},
);
test("should public tools work with list and call when unauthenticated", async () => {
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Public tool should be accessible
const result = await client.request(
{ method: "tools/list" },
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools[0].name).toBe("public-tool");
const response = await client.request(
{
method: "tools/call",
params: {
name: "public-tool",
arguments: {},
},
},
CallToolResultSchema,
);
expect(response.content).toEqual([
{
type: "text",
text: "Public tool response",
},
]);
});
test("should public tools work with list and call when unauthorized", async () => {
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Protected tool should be inaccessible when authenticated as a non-admin user
serverTransport.user = { role: "member" };
const result = await client.request(
{ method: "tools/list" },
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(1);
expect(result.tools[0].name).toBe("public-tool");
const response = await client.request(
{
method: "tools/call",
params: {
name: "public-tool",
arguments: {},
},
},
CallToolResultSchema,
);
expect(response.content).toEqual([
{
type: "text",
text: "Public tool response",
},
]);
});
test("should protected tools work with list and call when authorized", async () => {
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
serverTransport.user = { role: "admin" };
const result = await client.request(
{ method: "tools/list" },
ListToolsResultSchema,
);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe("public-tool");
expect(result.tools[1].name).toBe("protected-tool");
const response = await client.request(
{
method: "tools/call",
params: {
name: "protected-tool",
arguments: {},
},
},
CallToolResultSchema,
);
expect(response.content).toEqual([
{
type: "text",
text: "Protected tool response",
},
]);
});
});

View file

@ -56,17 +56,12 @@ export class McpServer {
*/
public readonly server: Server;
protected _registeredResources: { [uri: string]: RegisteredResource } = {};
protected _registeredResourceTemplates: {
private _registeredResources: { [uri: string]: RegisteredResource } = {};
private _registeredResourceTemplates: {
[name: string]: RegisteredResourceTemplate;
} = {};
protected _registeredTools: { [name: string]: RegisteredTool } = {};
protected _registeredPrompts: { [name: string]: RegisteredPrompt } = {};
protected _toolHandlersInitialized = false;
protected _completionHandlerInitialized = false;
protected _resourceHandlersInitialized = false;
protected _promptHandlersInitialized = false;
private _registeredTools: { [name: string]: RegisteredTool } = {};
private _registeredPrompts: { [name: string]: RegisteredPrompt } = {};
constructor(serverInfo: Implementation, options?: ServerOptions) {
this.server = new Server(serverInfo, options);
@ -88,11 +83,13 @@ export class McpServer {
await this.server.close();
}
private _toolHandlersInitialized = false;
private setToolRequestHandlers() {
if (this._toolHandlersInitialized) {
return;
}
this.server.assertCanSetRequestHandler(
ListToolsRequestSchema.shape.method.value,
);
@ -182,6 +179,8 @@ export class McpServer {
this._toolHandlersInitialized = true;
}
private _completionHandlerInitialized = false;
private setCompletionRequestHandler() {
if (this._completionHandlerInitialized) {
return;
@ -270,6 +269,8 @@ export class McpServer {
return createCompletionResult(suggestions);
}
private _resourceHandlersInitialized = false;
private setResourceRequestHandlers() {
if (this._resourceHandlersInitialized) {
return;
@ -367,10 +368,12 @@ export class McpServer {
);
this.setCompletionRequestHandler();
this._resourceHandlersInitialized = true;
}
private _promptHandlersInitialized = false;
private setPromptRequestHandlers() {
if (this._promptHandlersInitialized) {
return;
@ -437,7 +440,7 @@ export class McpServer {
);
this.setCompletionRequestHandler();
this._promptHandlersInitialized = true;
}
@ -769,7 +772,7 @@ type RegisteredPrompt = {
callback: PromptCallback<undefined | PromptArgsRawShape>;
};
export function promptArgumentsFromSchema(
function promptArgumentsFromSchema(
schema: ZodObject<PromptArgsRawShape>,
): PromptArgument[] {
return Object.entries(schema.shape).map(

View file

@ -19,7 +19,6 @@ export class SSEServerTransport implements Transport {
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
user?: unknown;
/**
* Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
@ -45,7 +44,7 @@ export class SSEServerTransport implements Transport {
this.res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});

View file

@ -21,7 +21,6 @@ export class StdioServerTransport implements Transport {
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
user?: unknown;
// Arrow functions to bind `this` properly, while maintaining function identity.
_ondata = (chunk: Buffer) => {
@ -74,7 +73,7 @@ export class StdioServerTransport implements Transport {
// This prevents interfering with other parts of the application that might be using stdin
this._stdin.pause();
}
// Clear the buffer and notify closure
this._readBuffer.clear();
this.onclose?.();

View file

@ -110,11 +110,6 @@ export type RequestHandlerExtra<SendRequestT extends Request,
*/
sessionId?: string;
/**
* The authenticated user, if available.
*/
user?: unknown;
/**
* Sends a notification that relates to the current request being handled.
*
@ -354,7 +349,6 @@ export abstract class Protocol<
const extra: RequestHandlerExtra<SendRequestT, SendNotificationT> = {
signal: abortController.signal,
sessionId: this._transport?.sessionId,
user: this._transport?.user,
sendNotification:
(notification) =>
this.notification(notification, { relatedRequestId: request.id }),

View file

@ -20,7 +20,7 @@ export class ReadBuffer {
return null;
}
const line = this._buffer.toString("utf8", 0, index);
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, '');
this._buffer = this._buffer.subarray(index + 1);
return deserializeMessage(line);
}

View file

@ -48,9 +48,4 @@ export interface Transport {
* The session ID generated for this connection.
*/
sessionId?: string;
/**
* The authenticated user for this transport session.
*/
user?: unknown;
}

View file

@ -24,11 +24,13 @@ describe("UriTemplate", () => {
expect(template.expand({ username: "fred" })).toBe(
"http://example.com/users/fred",
);
expect(template.variableNames).toEqual(['username'])
});
it("should handle multiple variables", () => {
const template = new UriTemplate("{x,y}");
expect(template.expand({ x: "1024", y: "768" })).toBe("1024,768");
expect(template.variableNames).toEqual(['x', 'y'])
});
it("should encode reserved characters", () => {
@ -43,6 +45,7 @@ describe("UriTemplate", () => {
it("should not encode reserved characters with + operator", () => {
const template = new UriTemplate("{+path}/here");
expect(template.expand({ path: "/foo/bar" })).toBe("/foo/bar/here");
expect(template.variableNames).toEqual(['path'])
});
});
@ -50,6 +53,7 @@ describe("UriTemplate", () => {
it("should add # prefix and not encode reserved chars", () => {
const template = new UriTemplate("X{#var}");
expect(template.expand({ var: "/test" })).toBe("X#/test");
expect(template.variableNames).toEqual(['var'])
});
});
@ -57,6 +61,7 @@ describe("UriTemplate", () => {
it("should add . prefix", () => {
const template = new UriTemplate("X{.var}");
expect(template.expand({ var: "test" })).toBe("X.test");
expect(template.variableNames).toEqual(['var'])
});
});
@ -64,6 +69,7 @@ describe("UriTemplate", () => {
it("should add / prefix", () => {
const template = new UriTemplate("X{/var}");
expect(template.expand({ var: "test" })).toBe("X/test");
expect(template.variableNames).toEqual(['var'])
});
});
@ -71,6 +77,7 @@ describe("UriTemplate", () => {
it("should add ? prefix and name=value format", () => {
const template = new UriTemplate("X{?var}");
expect(template.expand({ var: "test" })).toBe("X?var=test");
expect(template.variableNames).toEqual(['var'])
});
});
@ -78,6 +85,7 @@ describe("UriTemplate", () => {
it("should add & prefix and name=value format", () => {
const template = new UriTemplate("X{&var}");
expect(template.expand({ var: "test" })).toBe("X&var=test");
expect(template.variableNames).toEqual(['var'])
});
});
@ -133,6 +141,7 @@ describe("UriTemplate", () => {
resource: "users",
id: "123"
})).toBe("/api/v1/users/123");
expect(template.variableNames).toEqual(['version', 'resource', 'id'])
});
it("should handle query parameters with arrays", () => {
@ -140,6 +149,7 @@ describe("UriTemplate", () => {
expect(template.expand({
tags: ["nodejs", "typescript", "testing"]
})).toBe("/search?tags=nodejs,typescript,testing");
expect(template.variableNames).toEqual(['tags'])
});
it("should handle multiple query parameters", () => {
@ -149,6 +159,7 @@ describe("UriTemplate", () => {
page: "1",
limit: "10"
})).toBe("/search?q=test&page=1&limit=10");
expect(template.variableNames).toEqual(['q', 'page', 'limit'])
});
});
@ -161,18 +172,21 @@ describe("UriTemplate", () => {
resource: "users",
id: "123"
});
expect(template.variableNames).toEqual(['version', 'resource', 'id'])
});
it("should match query parameters", () => {
const template = new UriTemplate("/search{?q}");
const match = template.match("/search?q=test");
expect(match).toEqual({ q: "test" });
expect(template.variableNames).toEqual(['q'])
});
it("should match multiple query parameters", () => {
const template = new UriTemplate("/search{?q,page}");
const match = template.match("/search?q=test&page=1");
expect(match).toEqual({ q: "test", page: "1" });
expect(template.variableNames).toEqual(['q', 'page'])
});
it("should handle partial matches correctly", () => {
@ -229,17 +243,20 @@ describe("UriTemplate", () => {
it("should handle repeated operators", () => {
const template = new UriTemplate("{?a}{?b}{?c}");
expect(template.expand({ a: "1", b: "2", c: "3" })).toBe("?a=1&b=2&c=3");
expect(template.variableNames).toEqual(['a', 'b', 'c'])
});
it("should handle overlapping variable names", () => {
const template = new UriTemplate("{var}{vara}");
expect(template.expand({ var: "1", vara: "2" })).toBe("12");
expect(template.variableNames).toEqual(['var', 'vara'])
});
it("should handle empty segments", () => {
const template = new UriTemplate("///{a}////{b}////");
expect(template.expand({ a: "1", b: "2" })).toBe("///1////2////");
expect(template.match("///1////2////")).toEqual({ a: "1", b: "2" });
expect(template.variableNames).toEqual(['a', 'b'])
});
it("should handle maximum template expression limit", () => {

Some files were not shown because too many files have changed in this diff Show more