Skip to content

Commit 2ab2272

Browse files
committed
chore: reorganize imports and move test files
- Reorder devDependencies alphabetically in package.json - Update imports to use experimental/index.js instead of nested paths - Move EventStore import from streamableHttp.js to stores.js - Relocate FetchStreamableHTTPServerTransport tests to /test directory
1 parent 31b824d commit 2ab2272

File tree

12 files changed

+334
-519
lines changed

12 files changed

+334
-519
lines changed

package-lock.json

Lines changed: 162 additions & 316 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,8 @@
116116
},
117117
"devDependencies": {
118118
"@cfworker/json-schema": "^4.1.1",
119-
"@hono/node-server": "^1.19.6",
120-
"hono": "^4.10.7",
121119
"@eslint/js": "^9.39.1",
120+
"@hono/node-server": "^1.19.7",
122121
"@types/content-type": "^1.1.8",
123122
"@types/cors": "^2.8.17",
124123
"@types/cross-spawn": "^6.0.6",
@@ -132,6 +131,7 @@
132131
"eslint": "^9.8.0",
133132
"eslint-config-prettier": "^10.1.8",
134133
"eslint-plugin-n": "^17.23.1",
134+
"hono": "^4.10.7",
135135
"prettier": "3.6.2",
136136
"supertest": "^7.0.0",
137137
"tsx": "^4.16.5",

src/examples/server/expressFetchStreamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import express from 'express';
2222
import cors from 'cors';
2323
import { IncomingMessage, ServerResponse } from 'node:http';
2424
import { McpServer } from '../../server/mcp.js';
25-
import { FetchStreamableHTTPServerTransport } from '../../experimental/fetch-streamable-http/index.js';
25+
import { FetchStreamableHTTPServerTransport } from '../../experimental/index.js';
2626
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
2727
import { z } from 'zod';
2828

src/examples/server/honoFetchStreamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Hono } from 'hono';
3030
import { cors } from 'hono/cors';
3131
import { serve } from '@hono/node-server';
3232
import { McpServer } from '../../server/mcp.js';
33-
import { FetchStreamableHTTPServerTransport } from '../../experimental/fetch-streamable-http/index.js';
33+
import { FetchStreamableHTTPServerTransport } from '../../experimental/index.js';
3434
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
3535
import { z } from 'zod';
3636

src/examples/shared/inMemoryEventStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { JSONRPCMessage } from '../../types.js';
2-
import { EventStore } from '../../server/streamableHttp.js';
2+
import { EventStore } from '../../server/stores.js';
33

44
/**
55
* Simple in-memory implementation of the EventStore interface for resumability

src/experimental/fetch-streamable-http/index.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/experimental/fetch-streamable-http/fetchStreamableHttpServerTransport.ts renamed to src/experimental/fetchStreamableHttpServerTransport.ts

Lines changed: 22 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
* @experimental
99
*/
1010

11-
import { Transport } from '../../shared/transport.js';
11+
import { Transport } from '../shared/transport.js';
12+
import { EventStore } from '../server/stores.js';
13+
import { SessionStore } from './stores.js';
1214
import {
1315
MessageExtraInfo,
1416
RequestInfo,
@@ -19,105 +21,8 @@ import {
1921
JSONRPCMessage,
2022
JSONRPCMessageSchema,
2123
RequestId,
22-
SUPPORTED_PROTOCOL_VERSIONS,
23-
DEFAULT_NEGOTIATED_PROTOCOL_VERSION
24-
} from '../../types.js';
25-
26-
export type StreamId = string;
27-
export type EventId = string;
28-
29-
/**
30-
* Interface for resumability support via event storage
31-
*/
32-
export interface EventStore {
33-
/**
34-
* Stores an event for later retrieval
35-
* @param streamId ID of the stream the event belongs to
36-
* @param message The JSON-RPC message to store
37-
* @returns The generated event ID for the stored event
38-
*/
39-
storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise<EventId>;
40-
41-
/**
42-
* Get the stream ID associated with a given event ID.
43-
* @param eventId The event ID to look up
44-
* @returns The stream ID, or undefined if not found
45-
*
46-
* Optional: If not provided, the SDK will use the streamId returned by
47-
* replayEventsAfter for stream mapping.
48-
*/
49-
getStreamIdForEventId?(eventId: EventId): Promise<StreamId | undefined>;
50-
51-
replayEventsAfter(
52-
lastEventId: EventId,
53-
{
54-
send
55-
}: {
56-
send: (eventId: EventId, message: JSONRPCMessage) => Promise<void>;
57-
}
58-
): Promise<StreamId>;
59-
}
60-
61-
/**
62-
* Session state that can be persisted externally for serverless deployments.
63-
*/
64-
export interface SessionState {
65-
/** Whether the session has completed initialization */
66-
initialized: boolean;
67-
/** The negotiated protocol version */
68-
protocolVersion: string;
69-
/** Timestamp when the session was created */
70-
createdAt: number;
71-
}
72-
73-
/**
74-
* Interface for session storage in distributed/serverless deployments.
75-
*
76-
* In serverless environments (Lambda, Vercel, Cloudflare Workers), each request
77-
* may be handled by a different instance with no shared memory. The SessionStore
78-
* allows session state to be persisted externally (e.g., Redis, DynamoDB, KV).
79-
*
80-
* @example
81-
* ```typescript
82-
* // Cloudflare KV implementation
83-
* class KVSessionStore implements SessionStore {
84-
* constructor(private kv: KVNamespace) {}
85-
*
86-
* async get(sessionId: string) {
87-
* return this.kv.get(`session:${sessionId}`, 'json');
88-
* }
89-
* async save(sessionId: string, state: SessionState) {
90-
* await this.kv.put(`session:${sessionId}`, JSON.stringify(state), { expirationTtl: 3600 });
91-
* }
92-
* async delete(sessionId: string) {
93-
* await this.kv.delete(`session:${sessionId}`);
94-
* }
95-
* }
96-
* ```
97-
*/
98-
export interface SessionStore {
99-
/**
100-
* Retrieve session state by ID.
101-
* @param sessionId The session ID to look up
102-
* @returns The session state, or undefined if not found
103-
*/
104-
get(sessionId: string): Promise<SessionState | undefined>;
105-
106-
/**
107-
* Save session state.
108-
* Called when a session is initialized or updated.
109-
* @param sessionId The session ID
110-
* @param state The session state to persist
111-
*/
112-
save(sessionId: string, state: SessionState): Promise<void>;
113-
114-
/**
115-
* Delete session state.
116-
* Called when a session is explicitly closed via DELETE request.
117-
* @param sessionId The session ID to delete
118-
*/
119-
delete(sessionId: string): Promise<void>;
120-
}
24+
SUPPORTED_PROTOCOL_VERSIONS
25+
} from '../types.js';
12126

12227
/**
12328
* Internal stream mapping for managing SSE connections
@@ -211,19 +116,6 @@ export interface FetchStreamableHTTPServerTransportOptions {
211116
* to work across multiple serverless function invocations or instances.
212117
*
213118
* If not provided, session state is kept in-memory (single-instance mode).
214-
*
215-
* @example
216-
* ```typescript
217-
* // Redis session store
218-
* const transport = new FetchStreamableHTTPServerTransport({
219-
* sessionIdGenerator: () => crypto.randomUUID(),
220-
* sessionStore: {
221-
* get: async (id) => redis.get(`session:${id}`),
222-
* save: async (id, state) => redis.set(`session:${id}`, state, 'EX', 3600),
223-
* delete: async (id) => redis.del(`session:${id}`)
224-
* }
225-
* });
226-
* ```
227119
*/
228120
sessionStore?: SessionStore;
229121
}
@@ -675,11 +567,8 @@ export class FetchStreamableHTTPServerTransport implements Transport {
675567

676568
// Persist session state to external store if configured
677569
if (this.sessionId && this._sessionStore) {
678-
const protocolVersion = req.headers.get('mcp-protocol-version') ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
679570
await this._sessionStore.save(this.sessionId, {
680-
initialized: true,
681-
protocolVersion,
682-
createdAt: Date.now()
571+
initialized: true
683572
});
684573
}
685574

@@ -886,14 +775,27 @@ export class FetchStreamableHTTPServerTransport implements Transport {
886775
return undefined;
887776
}
888777

778+
/**
779+
* Validates the MCP-Protocol-Version header on incoming requests.
780+
*
781+
* This performs a simple check: if a version header is present, it must be
782+
* in the SUPPORTED_PROTOCOL_VERSIONS list. We do not track the negotiated
783+
* version or enforce version consistency across requests - the SDK handles
784+
* version negotiation during initialization, and we simply reject any
785+
* explicitly unsupported versions.
786+
*
787+
* - Header present and supported: Accept
788+
* - Header present and unsupported: 400 Bad Request
789+
* - Header missing: Accept (version validation is optional)
790+
*/
889791
private validateProtocolVersion(req: Request): Response | undefined {
890-
const protocolVersion = req.headers.get('mcp-protocol-version') ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
792+
const protocolVersion = req.headers.get('mcp-protocol-version');
891793

892-
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
794+
if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
893795
return this.createJsonErrorResponse(
894796
400,
895797
-32000,
896-
`Bad Request: Unsupported protocol version (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`
798+
`Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`
897799
);
898800
}
899801
return undefined;

src/experimental/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
*/
1313

1414
export * from './tasks/index.js';
15-
export * from './fetch-streamable-http/index.js';
15+
export * from './stores.js';
16+
export * from './fetchStreamableHttpServerTransport.js';

src/experimental/stores.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Experimental store interfaces for serverless deployments.
3+
*
4+
* @experimental
5+
*/
6+
7+
/**
8+
* Session state that can be persisted externally for serverless deployments.
9+
*/
10+
export interface SessionState {
11+
/** Whether the session has completed initialization */
12+
initialized: boolean;
13+
}
14+
15+
/**
16+
* Interface for session storage in distributed/serverless deployments.
17+
*
18+
* In serverless environments (Lambda, Vercel, Cloudflare Workers), each request
19+
* may be handled by a different instance with no shared memory. The SessionStore
20+
* allows session state to be persisted externally (e.g., Redis, DynamoDB, KV).
21+
*
22+
* @experimental
23+
* @example
24+
* ```typescript
25+
* // Cloudflare KV implementation
26+
* class KVSessionStore implements SessionStore {
27+
* constructor(private kv: KVNamespace) {}
28+
*
29+
* async get(sessionId: string) {
30+
* return this.kv.get(`session:${sessionId}`, 'json');
31+
* }
32+
* async save(sessionId: string, state: SessionState) {
33+
* await this.kv.put(`session:${sessionId}`, JSON.stringify(state), { expirationTtl: 3600 });
34+
* }
35+
* async delete(sessionId: string) {
36+
* await this.kv.delete(`session:${sessionId}`);
37+
* }
38+
* }
39+
* ```
40+
*/
41+
export interface SessionStore {
42+
/**
43+
* Retrieve session state by ID.
44+
* @param sessionId The session ID to look up
45+
* @returns The session state, or undefined if not found
46+
*/
47+
get(sessionId: string): Promise<SessionState | undefined>;
48+
49+
/**
50+
* Save session state.
51+
* Called when a session is initialized or updated.
52+
* @param sessionId The session ID
53+
* @param state The session state to persist
54+
*/
55+
save(sessionId: string, state: SessionState): Promise<void>;
56+
57+
/**
58+
* Delete session state.
59+
* Called when a session is explicitly closed via DELETE request.
60+
* @param sessionId The session ID to delete
61+
*/
62+
delete(sessionId: string): Promise<void>;
63+
}

src/server/stores.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Store interfaces for MCP server transports.
3+
*
4+
* These interfaces define contracts for external storage that enables
5+
* resumability support.
6+
*/
7+
8+
import { JSONRPCMessage } from '../types.js';
9+
10+
export type StreamId = string;
11+
export type EventId = string;
12+
13+
/**
14+
* Interface for resumability support via event storage.
15+
*
16+
* When provided to a transport, enables clients to reconnect and resume
17+
* receiving messages from where they left off using the Last-Event-ID header.
18+
*/
19+
export interface EventStore {
20+
/**
21+
* Stores an event for later retrieval.
22+
* @param streamId ID of the stream the event belongs to
23+
* @param message The JSON-RPC message to store
24+
* @returns The generated event ID for the stored event
25+
*/
26+
storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise<EventId>;
27+
28+
/**
29+
* Get the stream ID associated with a given event ID.
30+
* @param eventId The event ID to look up
31+
* @returns The stream ID, or undefined if not found
32+
*
33+
* Optional: If not provided, the SDK will use the streamId returned by
34+
* replayEventsAfter for stream mapping.
35+
*/
36+
getStreamIdForEventId?(eventId: EventId): Promise<StreamId | undefined>;
37+
38+
/**
39+
* Replays events that occurred after the given event ID.
40+
* @param lastEventId The last event ID the client received
41+
* @param options.send Callback to send each replayed event
42+
* @returns The stream ID for the replayed events
43+
*/
44+
replayEventsAfter(
45+
lastEventId: EventId,
46+
{
47+
send
48+
}: {
49+
send: (eventId: EventId, message: JSONRPCMessage) => Promise<void>;
50+
}
51+
): Promise<StreamId>;
52+
}

0 commit comments

Comments
 (0)