Skip to content

Commit 771f101

Browse files
committed
Merge branch 'main' of https://github.com/modelcontextprotocol/typescript-sdk into feat/elicitation-sampling-streaming
2 parents ca06490 + 4d6c3b8 commit 771f101

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1853
-606
lines changed

.github/workflows/main.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,19 @@ jobs:
6767
id: npm-tag
6868
run: |
6969
VERSION=$(node -p "require('./package.json').version")
70+
# Check if this is a beta release
7071
if [[ "$VERSION" == *"-beta"* ]]; then
7172
echo "tag=--tag beta" >> $GITHUB_OUTPUT
73+
# Check if this release is from a non-main branch (patch/maintenance release)
74+
elif [[ "${{ github.event.release.target_commitish }}" != "main" ]]; then
75+
# Use "release-X.Y" as tag for old branch releases (e.g., "release-1.23" for 1.23.x)
76+
# npm tags are mutable pointers to versions (like "latest" pointing to 1.24.3).
77+
# Using "release-1.23" means users can `npm install @modelcontextprotocol/[email protected]`
78+
# to get the latest patch on that minor version, and the tag updates if we
79+
# release 1.23.2, 1.23.3, etc.
80+
# Note: Can't use "v1.23" because npm rejects tags that look like semver ranges.
81+
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
82+
echo "tag=--tag release-${MAJOR_MINOR}" >> $GITHUB_OUTPUT
7283
else
7384
echo "tag=" >> $GITHUB_OUTPUT
7485
fi

docs/client.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Examples:
5151
- [`simpleOAuthClient.ts`](../src/examples/client/simpleOAuthClient.ts)
5252
- [`simpleOAuthClientProvider.ts`](../src/examples/client/simpleOAuthClientProvider.ts)
5353
- [`simpleClientCredentials.ts`](../src/examples/client/simpleClientCredentials.ts)
54-
- Server-side auth demo: [`demoInMemoryOAuthProvider.ts`](../src/examples/server/demoInMemoryOAuthProvider.ts)
54+
- Server-side auth demo: [`demoInMemoryOAuthProvider.ts`](../src/examples/server/demoInMemoryOAuthProvider.ts) (tests live under `test/examples/server/demoInMemoryOAuthProvider.test.ts`)
5555

5656
These examples show how to:
5757

package-lock.json

Lines changed: 9 additions & 2 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.24.2",
3+
"version": "1.24.3",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -96,6 +96,7 @@
9696
"express": "^5.0.1",
9797
"express-rate-limit": "^7.5.0",
9898
"jose": "^6.1.1",
99+
"json-schema-typed": "^8.0.2",
99100
"pkce-challenge": "^5.0.0",
100101
"raw-body": "^3.0.0",
101102
"zod": "^3.25 || ^4.0",

src/client/index.ts

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ import {
4242
ElicitRequestSchema,
4343
CreateTaskResultSchema,
4444
CreateMessageRequestSchema,
45-
CreateMessageResultSchema
45+
CreateMessageResultSchema,
46+
ToolListChangedNotificationSchema,
47+
PromptListChangedNotificationSchema,
48+
ResourceListChangedNotificationSchema,
49+
ListChangedOptions,
50+
ListChangedOptionsBaseSchema,
51+
type ListChangedHandlers
4652
} from '../types.js';
4753
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
4854
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js';
@@ -87,14 +93,20 @@ function applyElicitationDefaults(schema: JsonSchemaType | undefined, data: unkn
8793

8894
if (Array.isArray(schema.anyOf)) {
8995
for (const sub of schema.anyOf) {
90-
applyElicitationDefaults(sub, data);
96+
// Skip boolean schemas (true/false are valid JSON Schemas but have no defaults)
97+
if (typeof sub !== 'boolean') {
98+
applyElicitationDefaults(sub, data);
99+
}
91100
}
92101
}
93102

94103
// Combine schemas
95104
if (Array.isArray(schema.oneOf)) {
96105
for (const sub of schema.oneOf) {
97-
applyElicitationDefaults(sub, data);
106+
// Skip boolean schemas (true/false are valid JSON Schemas but have no defaults)
107+
if (typeof sub !== 'boolean') {
108+
applyElicitationDefaults(sub, data);
109+
}
98110
}
99111
}
100112
}
@@ -163,6 +175,34 @@ export type ClientOptions = ProtocolOptions & {
163175
* ```
164176
*/
165177
jsonSchemaValidator?: jsonSchemaValidator;
178+
179+
/**
180+
* Configure handlers for list changed notifications (tools, prompts, resources).
181+
*
182+
* @example
183+
* ```typescript
184+
* const client = new Client(
185+
* { name: 'my-client', version: '1.0.0' },
186+
* {
187+
* listChanged: {
188+
* tools: {
189+
* onChanged: (error, tools) => {
190+
* if (error) {
191+
* console.error('Failed to refresh tools:', error);
192+
* return;
193+
* }
194+
* console.log('Tools updated:', tools);
195+
* }
196+
* },
197+
* prompts: {
198+
* onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
199+
* }
200+
* }
201+
* }
202+
* );
203+
* ```
204+
*/
205+
listChanged?: ListChangedHandlers;
166206
};
167207

168208
/**
@@ -204,6 +244,8 @@ export class Client<
204244
private _cachedKnownTaskTools: Set<string> = new Set();
205245
private _cachedRequiredTaskTools: Set<string> = new Set();
206246
private _experimental?: { tasks: ExperimentalClientTasks<RequestT, NotificationT, ResultT> };
247+
private _listChangedDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
248+
private _pendingListChangedConfig?: ListChangedHandlers;
207249

208250
/**
209251
* Initializes this client with the given name and version information.
@@ -215,6 +257,40 @@ export class Client<
215257
super(options);
216258
this._capabilities = options?.capabilities ?? {};
217259
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
260+
261+
// Store list changed config for setup after connection (when we know server capabilities)
262+
if (options?.listChanged) {
263+
this._pendingListChangedConfig = options.listChanged;
264+
}
265+
}
266+
267+
/**
268+
* Set up handlers for list changed notifications based on config and server capabilities.
269+
* This should only be called after initialization when server capabilities are known.
270+
* Handlers are silently skipped if the server doesn't advertise the corresponding listChanged capability.
271+
* @internal
272+
*/
273+
private _setupListChangedHandlers(config: ListChangedHandlers): void {
274+
if (config.tools && this._serverCapabilities?.tools?.listChanged) {
275+
this._setupListChangedHandler('tools', ToolListChangedNotificationSchema, config.tools, async () => {
276+
const result = await this.listTools();
277+
return result.tools;
278+
});
279+
}
280+
281+
if (config.prompts && this._serverCapabilities?.prompts?.listChanged) {
282+
this._setupListChangedHandler('prompts', PromptListChangedNotificationSchema, config.prompts, async () => {
283+
const result = await this.listPrompts();
284+
return result.prompts;
285+
});
286+
}
287+
288+
if (config.resources && this._serverCapabilities?.resources?.listChanged) {
289+
this._setupListChangedHandler('resources', ResourceListChangedNotificationSchema, config.resources, async () => {
290+
const result = await this.listResources();
291+
return result.resources;
292+
});
293+
}
218294
}
219295

220296
/**
@@ -442,6 +518,12 @@ export class Client<
442518
await this.notification({
443519
method: 'notifications/initialized'
444520
});
521+
522+
// Set up list changed handlers now that we know server capabilities
523+
if (this._pendingListChangedConfig) {
524+
this._setupListChangedHandlers(this._pendingListChangedConfig);
525+
this._pendingListChangedConfig = undefined;
526+
}
445527
} catch (error) {
446528
// Disconnect if initialization fails.
447529
void this.close();
@@ -757,6 +839,66 @@ export class Client<
757839
return result;
758840
}
759841

842+
/**
843+
* Set up a single list changed handler.
844+
* @internal
845+
*/
846+
private _setupListChangedHandler<T>(
847+
listType: string,
848+
notificationSchema: { shape: { method: { value: string } } },
849+
options: ListChangedOptions<T>,
850+
fetcher: () => Promise<T[]>
851+
): void {
852+
// Validate options using Zod schema (validates autoRefresh and debounceMs)
853+
const parseResult = ListChangedOptionsBaseSchema.safeParse(options);
854+
if (!parseResult.success) {
855+
throw new Error(`Invalid ${listType} listChanged options: ${parseResult.error.message}`);
856+
}
857+
858+
// Validate callback
859+
if (typeof options.onChanged !== 'function') {
860+
throw new Error(`Invalid ${listType} listChanged options: onChanged must be a function`);
861+
}
862+
863+
const { autoRefresh, debounceMs } = parseResult.data;
864+
const { onChanged } = options;
865+
866+
const refresh = async () => {
867+
if (!autoRefresh) {
868+
onChanged(null, null);
869+
return;
870+
}
871+
872+
try {
873+
const items = await fetcher();
874+
onChanged(null, items);
875+
} catch (e) {
876+
const error = e instanceof Error ? e : new Error(String(e));
877+
onChanged(error, null);
878+
}
879+
};
880+
881+
const handler = () => {
882+
if (debounceMs) {
883+
// Clear any pending debounce timer for this list type
884+
const existingTimer = this._listChangedDebounceTimers.get(listType);
885+
if (existingTimer) {
886+
clearTimeout(existingTimer);
887+
}
888+
889+
// Set up debounced refresh
890+
const timer = setTimeout(refresh, debounceMs);
891+
this._listChangedDebounceTimers.set(listType, timer);
892+
} else {
893+
// No debounce, refresh immediately
894+
refresh();
895+
}
896+
};
897+
898+
// Register notification handler
899+
this.setNotificationHandler(notificationSchema as AnyObjectSchema, handler);
900+
}
901+
760902
async sendRootsListChanged() {
761903
return this.notification({ method: 'notifications/roots/list_changed' });
762904
}

src/client/sse.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ export class SSEClientTransport implements Transport {
281281

282282
throw new Error(`Error POSTing to endpoint (HTTP ${response.status}): ${text}`);
283283
}
284+
285+
// Release connection - POST responses don't have content we need
286+
await response.body?.cancel();
284287
} catch (error) {
285288
this.onerror?.(error as Error);
286289
throw error;

src/client/streamableHttp.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,12 @@ export class StreamableHTTPClientTransport implements Transport {
592592
this.onmessage?.(msg);
593593
}
594594
} else {
595+
await response.body?.cancel();
595596
throw new StreamableHTTPError(-1, `Unexpected content type: ${contentType}`);
596597
}
598+
} else {
599+
// No requests in message but got 200 OK - still need to release connection
600+
await response.body?.cancel();
597601
}
598602
} catch (error) {
599603
this.onerror?.(error as Error);

src/examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ npx tsx src/examples/client/elicitationUrlExample.ts
155155

156156
#### Deprecated SSE Transport
157157

158-
A server that implements the deprecated HTTP+SSE transport (protocol version 2024-11-05). This example only used for testing backwards compatibility for clients.
158+
A server that implements the deprecated HTTP+SSE transport (protocol version 2024-11-05). This example is only used for testing backwards compatibility for clients.
159159

160160
- Two separate endpoints: `/mcp` for the SSE stream (GET) and `/messages` for client messages (POST)
161161
- Tool implementation with a `start-notification-stream` tool that demonstrates sending periodic notifications

src/server/mcp.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { Transport } from '../shared/transport.js';
6262
import { validateAndWarnToolName } from '../shared/toolNameValidation.js';
6363
import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.js';
6464
import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js';
65+
import { ZodOptional } from 'zod';
6566

6667
/**
6768
* High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
@@ -557,8 +558,6 @@ export class McpServer {
557558
throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
558559
});
559560

560-
this.setCompletionRequestHandler();
561-
562561
this._resourceHandlersInitialized = true;
563562
}
564563

@@ -623,8 +622,6 @@ export class McpServer {
623622
}
624623
});
625624

626-
this.setCompletionRequestHandler();
627-
628625
this._promptHandlersInitialized = true;
629626
}
630627

@@ -815,6 +812,14 @@ export class McpServer {
815812
}
816813
};
817814
this._registeredResourceTemplates[name] = registeredResourceTemplate;
815+
816+
// If the resource template has any completion callbacks, enable completions capability
817+
const variableNames = template.uriTemplate.variableNames;
818+
const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v));
819+
if (hasCompleter) {
820+
this.setCompletionRequestHandler();
821+
}
822+
818823
return registeredResourceTemplate;
819824
}
820825

@@ -848,6 +853,18 @@ export class McpServer {
848853
}
849854
};
850855
this._registeredPrompts[name] = registeredPrompt;
856+
857+
// If any argument uses a Completable schema, enable completions capability
858+
if (argsSchema) {
859+
const hasCompletable = Object.values(argsSchema).some(field => {
860+
const inner: unknown = field instanceof ZodOptional ? field._def?.innerType : field;
861+
return isCompletable(inner);
862+
});
863+
if (hasCompletable) {
864+
this.setCompletionRequestHandler();
865+
}
866+
}
867+
851868
return registeredPrompt;
852869
}
853870

@@ -889,6 +906,7 @@ export class McpServer {
889906
if (typeof updates.title !== 'undefined') registeredTool.title = updates.title;
890907
if (typeof updates.description !== 'undefined') registeredTool.description = updates.description;
891908
if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = objectFromShape(updates.paramsSchema);
909+
if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = objectFromShape(updates.outputSchema);
892910
if (typeof updates.callback !== 'undefined') registeredTool.handler = updates.callback;
893911
if (typeof updates.annotations !== 'undefined') registeredTool.annotations = updates.annotations;
894912
if (typeof updates._meta !== 'undefined') registeredTool._meta = updates._meta;

0 commit comments

Comments
 (0)