Skip to content

Commit d18c9b3

Browse files
Merge branch 'main' into fweinberger/reconnection-scheduler
2 parents 3dcd989 + 64897f7 commit d18c9b3

File tree

17 files changed

+308
-35
lines changed

17 files changed

+308
-35
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
Abort in-flight request handlers when the connection closes. Previously, request handlers would continue running after the transport disconnected, wasting resources and preventing proper cleanup. Also fixes `InMemoryTransport.close()` firing `onclose` twice on the initiating side.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
Add missing `size` field to `ResourceSchema` to match the MCP specification
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
'@modelcontextprotocol/client': patch
4+
'@modelcontextprotocol/server': patch
5+
---
6+
7+
Convert remaining capability-assertion throws to `SdkError(SdkErrorCode.CapabilityNotSupported, ...)`. Follow-up to #1454 which missed `Client.assertCapability()`, the task capability helpers in `experimental/tasks/helpers.ts`, and the sampling/elicitation capability checks in `experimental/tasks/server.ts`.

.changeset/stdio-skip-non-json.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
`ReadBuffer.readMessage()` now silently skips non-JSON lines instead of throwing `SyntaxError`. This prevents noisy `onerror` callbacks when hot-reload tools (tsx, nodemon) write debug output like "Gracefully restarting..." to stdout. Lines that parse as JSON but fail JSONRPC schema validation still throw.

docs/server.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,26 @@ server.registerTool(
330330
>
331331
> For protocol details, see [Logging](https://modelcontextprotocol.io/specification/latest/server/utilities/logging) in the MCP specification.
332332
333+
## Instructions
334+
335+
Pass an `instructions` string in the server options to describe how to use the server and its features. This can be used by clients to improve the LLM's understanding of available tools, resources, and prompts. It can be thought of like a "hint" to the model — for example, a client MAY add it to the system prompt. See [Instructions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#instructions) in the MCP specification.
336+
337+
```ts source="../examples/server/src/serverGuide.examples.ts#instructions_basic"
338+
const server = new McpServer(
339+
{
340+
name: 'multi-tool-server',
341+
version: '1.0.0'
342+
},
343+
{
344+
instructions: `This server provides data-pipeline tools. Always call "validate-schema"
345+
before calling "transform-data" to avoid runtime errors.`
346+
}
347+
);
348+
```
349+
350+
> [!TIP]
351+
> Use instructions for cross-tool relationships, workflow ordering, and constraints that individual tool descriptions cannot express on their own.
352+
333353
## Server‑initiated requests
334354

335355
MCP is bidirectional — servers can also send requests *to* the client during tool execution, as long as the client declares matching capabilities.

examples/server/src/serverGuide.examples.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ import type { CallToolResult, ResourceLink } from '@modelcontextprotocol/server'
1515
import { completable, McpServer, ResourceTemplate, StdioServerTransport } from '@modelcontextprotocol/server';
1616
import * as z from 'zod/v4';
1717

18+
// ---------------------------------------------------------------------------
19+
// Instructions
20+
// ---------------------------------------------------------------------------
21+
22+
/** Example: Providing server instructions to guide LLM usage. */
23+
function instructions_basic() {
24+
//#region instructions_basic
25+
const server = new McpServer(
26+
{
27+
name: 'multi-tool-server',
28+
version: '1.0.0'
29+
},
30+
{
31+
instructions: `This server provides data-pipeline tools. Always call "validate-schema"
32+
before calling "transform-data" to avoid runtime errors.`
33+
}
34+
);
35+
//#endregion instructions_basic
36+
return server;
37+
}
38+
1839
// ---------------------------------------------------------------------------
1940
// Tools, resources, and prompts
2041
// ---------------------------------------------------------------------------
@@ -373,6 +394,7 @@ function dnsRebinding_allowedHosts() {
373394
}
374395

375396
// Suppress unused-function warnings (functions exist solely for type-checking)
397+
void instructions_basic;
376398
void registerTool_basic;
377399
void registerTool_resourceLink;
378400
void registerTool_logging;

packages/client/src/client/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ export class Client extends Protocol<ClientContext> {
456456

457457
protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
458458
if (!this._serverCapabilities?.[capability]) {
459-
throw new Error(`Server does not support ${capability} (required for ${method})`);
459+
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support ${capability} (required for ${method})`);
460460
}
461461
}
462462

packages/core/src/experimental/tasks/helpers.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* @experimental
66
*/
77

8+
import { SdkError, SdkErrorCode } from '../../errors/sdkErrors.js';
9+
810
/**
911
* Type representing the task requests capability structure.
1012
* This is derived from `ClientTasksCapability.requests` and `ServerTasksCapability.requests`.
@@ -22,7 +24,7 @@ interface TaskRequestsCapability {
2224
* @param requests - The task requests capability object
2325
* @param method - The method being checked
2426
* @param entityName - `'Server'` or `'Client'` for error messages
25-
* @throws Error if the capability is not supported
27+
* @throws {@linkcode SdkError} with {@linkcode SdkErrorCode.CapabilityNotSupported} if the capability is not supported
2628
*
2729
* @experimental
2830
*/
@@ -32,13 +34,16 @@ export function assertToolsCallTaskCapability(
3234
entityName: 'Server' | 'Client'
3335
): void {
3436
if (!requests) {
35-
throw new Error(`${entityName} does not support task creation (required for ${method})`);
37+
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation (required for ${method})`);
3638
}
3739

3840
switch (method) {
3941
case 'tools/call': {
4042
if (!requests.tools?.call) {
41-
throw new Error(`${entityName} does not support task creation for tools/call (required for ${method})`);
43+
throw new SdkError(
44+
SdkErrorCode.CapabilityNotSupported,
45+
`${entityName} does not support task creation for tools/call (required for ${method})`
46+
);
4247
}
4348
break;
4449
}
@@ -57,7 +62,7 @@ export function assertToolsCallTaskCapability(
5762
* @param requests - The task requests capability object
5863
* @param method - The method being checked
5964
* @param entityName - `'Server'` or `'Client'` for error messages
60-
* @throws Error if the capability is not supported
65+
* @throws {@linkcode SdkError} with {@linkcode SdkErrorCode.CapabilityNotSupported} if the capability is not supported
6166
*
6267
* @experimental
6368
*/
@@ -67,20 +72,26 @@ export function assertClientRequestTaskCapability(
6772
entityName: 'Server' | 'Client'
6873
): void {
6974
if (!requests) {
70-
throw new Error(`${entityName} does not support task creation (required for ${method})`);
75+
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation (required for ${method})`);
7176
}
7277

7378
switch (method) {
7479
case 'sampling/createMessage': {
7580
if (!requests.sampling?.createMessage) {
76-
throw new Error(`${entityName} does not support task creation for sampling/createMessage (required for ${method})`);
81+
throw new SdkError(
82+
SdkErrorCode.CapabilityNotSupported,
83+
`${entityName} does not support task creation for sampling/createMessage (required for ${method})`
84+
);
7785
}
7886
break;
7987
}
8088

8189
case 'elicitation/create': {
8290
if (!requests.elicitation?.create) {
83-
throw new Error(`${entityName} does not support task creation for elicitation/create (required for ${method})`);
91+
throw new SdkError(
92+
SdkErrorCode.CapabilityNotSupported,
93+
`${entityName} does not support task creation for elicitation/create (required for ${method})`
94+
);
8495
}
8596
break;
8697
}

packages/core/src/shared/protocol.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -457,8 +457,11 @@ export abstract class Protocol<ContextT extends BaseContext> {
457457
this._transport = transport;
458458
const _onclose = this.transport?.onclose;
459459
this._transport.onclose = () => {
460-
_onclose?.();
461-
this._onclose();
460+
try {
461+
_onclose?.();
462+
} finally {
463+
this._onclose();
464+
}
462465
};
463466

464467
const _onerror = this.transport?.onerror;
@@ -494,13 +497,28 @@ export abstract class Protocol<ContextT extends BaseContext> {
494497
this._taskManager.onClose();
495498
this._pendingDebouncedNotifications.clear();
496499

500+
for (const info of this._timeoutInfo.values()) {
501+
clearTimeout(info.timeoutId);
502+
}
503+
this._timeoutInfo.clear();
504+
505+
const requestHandlerAbortControllers = this._requestHandlerAbortControllers;
506+
this._requestHandlerAbortControllers = new Map();
507+
497508
const error = new SdkError(SdkErrorCode.ConnectionClosed, 'Connection closed');
498509

499510
this._transport = undefined;
500-
this.onclose?.();
501511

502-
for (const handler of responseHandlers.values()) {
503-
handler(error);
512+
try {
513+
this.onclose?.();
514+
} finally {
515+
for (const handler of responseHandlers.values()) {
516+
handler(error);
517+
}
518+
519+
for (const controller of requestHandlerAbortControllers.values()) {
520+
controller.abort(error);
521+
}
504522
}
505523
}
506524

@@ -642,7 +660,9 @@ export abstract class Protocol<ContextT extends BaseContext> {
642660
)
643661
.catch(error => this._onerror(new Error(`Failed to send response: ${error}`)))
644662
.finally(() => {
645-
this._requestHandlerAbortControllers.delete(request.id);
663+
if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
664+
this._requestHandlerAbortControllers.delete(request.id);
665+
}
646666
});
647667
}
648668

packages/core/src/shared/stdio.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,28 @@ export class ReadBuffer {
1212
}
1313

1414
readMessage(): JSONRPCMessage | null {
15-
if (!this._buffer) {
16-
return null;
15+
while (this._buffer) {
16+
const index = this._buffer.indexOf('\n');
17+
if (index === -1) {
18+
return null;
19+
}
20+
21+
const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, '');
22+
this._buffer = this._buffer.subarray(index + 1);
23+
24+
try {
25+
return deserializeMessage(line);
26+
} catch (error) {
27+
// Skip non-JSON lines (e.g., debug output from hot-reload tools like
28+
// tsx or nodemon that write to stdout). Schema validation errors still
29+
// throw so malformed-but-valid-JSON messages surface via onerror.
30+
if (error instanceof SyntaxError) {
31+
continue;
32+
}
33+
throw error;
34+
}
1735
}
18-
19-
const index = this._buffer.indexOf('\n');
20-
if (index === -1) {
21-
return null;
22-
}
23-
24-
const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, '');
25-
this._buffer = this._buffer.subarray(index + 1);
26-
return deserializeMessage(line);
36+
return null;
2737
}
2838

2939
clear(): void {

0 commit comments

Comments
 (0)