diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ee735c..331810c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ### Conformance and release readiness +- Added a manual TypeScript SDK 2026 RC interop fixture pinned to the upstream + PR #2327 preview package, covering modern negotiation, `tools/list`, and + `tools/call` against the Dart 2026 RC conformance server. +- Marked `server/discover` as a 2026 cacheable result so stateless responses + include default `ttlMs` and `cacheScope` hints. - Updated official conformance gates to `@modelcontextprotocol/conformance@0.2.0-alpha.4`, with 2026 RC runs pinned to `2026-07-28`, the full 2026 server scenario list covered in CI, the 2026 diff --git a/doc/interoperability.md b/doc/interoperability.md index cbbc701c..77b0384c 100644 --- a/doc/interoperability.md +++ b/doc/interoperability.md @@ -21,6 +21,7 @@ For requirement-level MCP 2025-11-25 coverage, see the | Dart client -> TypeScript SDK server | Streamable HTTP | `2025-11-25` | [`test/interop/dart_client_with_ts_server_test.dart`](../test/interop/dart_client_with_ts_server_test.dart), [`test/interop/ts/`](../test/interop/ts/) | Verified | Covers tool calls and stale preconfigured session-id recovery. | | TypeScript SDK client -> Dart server | stdio | `2025-11-25` | [`test/interop/ts_client_with_dart_server_test.dart`](../test/interop/ts_client_with_dart_server_test.dart), [`test/interop/test_dart_server.dart`](../test/interop/test_dart_server.dart) | Verified | Runs the compiled TypeScript client fixture against a Dart server process and checks that an official TS client can list tools immediately after the lifecycle handshake. | | TypeScript SDK client -> Dart server | Streamable HTTP | `2025-11-25` | [`test/interop/ts_client_with_dart_server_test.dart`](../test/interop/ts_client_with_dart_server_test.dart), [`test/interop/test_dart_server.dart`](../test/interop/test_dart_server.dart) | Verified | Includes official TS Streamable HTTP client lifecycle coverage, pre-`initialized` operation rejection, GET SSE streams, and `Last-Event-ID` replay behavior. | +| TypeScript SDK preview client -> Dart server | Streamable HTTP | `2026-07-28` draft/RC | [`test/interop/ts_2026_rc/`](../test/interop/ts_2026_rc/), [`tool/testing/run_ts_2026_rc_interop.dart`](../tool/testing/run_ts_2026_rc_interop.dart) | Experimental manual check | Uses a pinned `pkg.pr.new` preview from TypeScript SDK PR #2327 because published TS packages do not yet advertise `2026-07-28`. Covers modern negotiation, `tools/list`, and `tools/call` against the Dart 2026 RC conformance server. Not a CI gate yet. | | Dart client -> Python MCP server | stdio | Server-dependent | [`doc/transports.md`](transports.md#connect-to-python-server) | Documented recipe | The transport can spawn Python servers over stdio, but this repo does not yet include an automated Python SDK fixture. | | Flutter/Web client -> Dart server | Streamable HTTP | `2025-11-25` | [`example/flutter_http_client/`](../example/flutter_http_client/), [`doc/flutter-recipes.md`](flutter-recipes.md) | Documented recipe | Flutter Web cannot spawn stdio servers; use Streamable HTTP or another browser-safe transport. | | MCP Apps host/client metadata | stdio or Streamable HTTP | `2025-11-25` plus `io.modelcontextprotocol/ui` extension | [`doc/mcp-apps.md`](mcp-apps.md), [`example/mcp_apps_helpers_server.dart`](../example/mcp_apps_helpers_server.dart), [`test/types/mcp_ui_test.dart`](../test/types/mcp_ui_test.dart), [`test/server/mcp_ui_test.dart`](../test/server/mcp_ui_test.dart) | Verified | Verified coverage is limited to SDK metadata helpers, serialization, and checked-in examples; host rendering behavior varies by host, so verify UI metadata against your target host. | @@ -42,6 +43,20 @@ dart test --tags interop If the compiled fixtures are missing, local test runs skip the interop groups; CI should fail when required fixtures are unavailable. +The TypeScript 2026 RC fixture is manual while the upstream SDK support remains +unreleased and split across preview PRs: + +```bash +# From repository root +cd test/interop/ts_2026_rc +npm install +cd ../../.. +dart run tool/testing/run_ts_2026_rc_interop.dart +``` + +This starts the Dart 2026 RC conformance server and runs the pinned TypeScript +SDK preview client against it. + The CLI spec conformance gate covers raw-wire negative cases that do not need a cross-SDK fixture, including stable MCP 2025-11-25 checks and MCP 2026-07-28 RC stateless/discovery/task-extension checks: @@ -65,6 +80,8 @@ When adding a new interoperability claim: ## Known gaps worth tracking - Automated Python SDK fixture coverage. +- CI promotion for the TypeScript 2026 RC interop fixture after the TypeScript + SDK publishes a 2026-compatible alpha package. - Host-specific MCP Apps rendering compatibility notes. - More OAuth-protected remote server scenarios beyond the checked-in examples. - A broader compatibility table once additional SDKs expose stable 2025-11-25 fixtures. diff --git a/doc/mcp-2026-rc.md b/doc/mcp-2026-rc.md index 37b17aaf..69b9254a 100644 --- a/doc/mcp-2026-rc.md +++ b/doc/mcp-2026-rc.md @@ -130,7 +130,7 @@ Before creating follow-up dev tags from `dev/2026-07-28-rc`, run: ```sh dart analyze dart run test/conformance/run_2025_server_conformance.dart -npx -y @modelcontextprotocol/conformance@0.2.0-alpha.3 client \ +npx -y @modelcontextprotocol/conformance@0.2.0-alpha.4 client \ --command "dart run test/conformance/mcp_2026_rc_client.dart" \ --suite all \ --spec-version 2025-11-25 @@ -142,10 +142,16 @@ dart run tool/validate_cli_publish.dart ``` The `run_2026_rc_server_conformance.dart` gate runs the full -`@modelcontextprotocol/conformance@0.2.0-alpha.3` server scenario list for +`@modelcontextprotocol/conformance@0.2.0-alpha.4` server scenario list for `--spec-version 2026-07-28`, including the stable-style tool, resource, prompt, completion, and JSON Schema scenarios that the alpha package tags for the RC. +For cross-SDK smoke coverage against the TypeScript SDK 2026 preview client, +run the manual fixture documented in +[`doc/interoperability.md`](interoperability.md#running-interop-checks-locally). +Keep that fixture out of CI until upstream publishes a 2026-compatible alpha +package instead of requiring a `pkg.pr.new` PR preview. + For dev packages, keep package documentation links pointed at `dev/2026-07-28-rc` until the draft work is ready to merge back to `main`. Restore those links to `main` as part of the final spec release prep. diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index 1809540d..8ab76bbf 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -873,6 +873,7 @@ class Server extends Protocol { bool _requiresCacheableResult(String method) { return switch (method) { + Method.serverDiscover || Method.toolsList || Method.promptsList || Method.resourcesList || diff --git a/lib/src/shared/protocol.dart b/lib/src/shared/protocol.dart index 0f8eb67d..8ea09720 100644 --- a/lib/src/shared/protocol.dart +++ b/lib/src/shared/protocol.dart @@ -16,6 +16,7 @@ bool _isProgressToken(Object? token) => (token is double && token.isFinite && token == token.truncateToDouble()); const Set _statelessCacheableResultMethods = { + Method.serverDiscover, Method.toolsList, Method.promptsList, Method.resourcesList, diff --git a/lib/src/types/initialization.dart b/lib/src/types/initialization.dart index ee6ab405..81f93c57 100644 --- a/lib/src/types/initialization.dart +++ b/lib/src/types/initialization.dart @@ -1345,7 +1345,7 @@ class InitializeResult implements BaseResultData { } /// Result data for a successful `server/discover` request. -class DiscoverResult implements BaseResultData { +class DiscoverResult implements CacheableResultData { /// Result discriminator used by the 2026 result model. final String resultType; @@ -1361,6 +1361,14 @@ class DiscoverResult implements BaseResultData { /// Instructions describing how to use the server and its features. final String? instructions; + /// How long, in milliseconds, the client may consider this result fresh. + @override + final int? ttlMs; + + /// Intended cache visibility: `public` or `private`. + @override + final String? cacheScope; + /// Optional metadata. @override final Map? meta; @@ -1371,6 +1379,8 @@ class DiscoverResult implements BaseResultData { required this.capabilities, required this.serverInfo, this.instructions, + this.ttlMs, + this.cacheScope, this.meta, }); @@ -1407,12 +1417,19 @@ class DiscoverResult implements BaseResultData { json['instructions'], 'DiscoverResult.instructions', ), + ttlMs: readOptionalTtlMs(json['ttlMs'], 'DiscoverResult.ttlMs'), + cacheScope: readOptionalCacheScope( + json['cacheScope'], + 'DiscoverResult.cacheScope', + ), meta: readOptionalJsonObject(json['_meta'], 'DiscoverResult._meta'), ); } @override Map toJson() { + validateTtlMs(ttlMs, 'DiscoverResult.ttlMs'); + validateCacheScope(cacheScope, 'DiscoverResult.cacheScope'); if (resultType != resultTypeComplete) { throw ArgumentError.value( resultType, @@ -1427,6 +1444,8 @@ class DiscoverResult implements BaseResultData { 'capabilities': capabilities.toJson(omitLegacyTasks: true), 'serverInfo': serverInfo.toJson(), if (instructions != null) 'instructions': instructions, + if (ttlMs != null) 'ttlMs': ttlMs, + if (cacheScope != null) 'cacheScope': cacheScope, if (meta != null) '_meta': readJsonObject(meta, 'DiscoverResult._meta'), }; } diff --git a/packages/mcp_dart_cli/lib/src/conformance_runner.dart b/packages/mcp_dart_cli/lib/src/conformance_runner.dart index 425b5cae..a3262ab2 100644 --- a/packages/mcp_dart_cli/lib/src/conformance_runner.dart +++ b/packages/mcp_dart_cli/lib/src/conformance_runner.dart @@ -922,6 +922,8 @@ class _DiscoveringConformanceTransport extends Transport 'name': 'conformance-server', 'version': '1.0.0', }, + 'ttlMs': 0, + 'cacheScope': _cacheScopePrivate, }, ), ); @@ -1480,18 +1482,21 @@ Future _httpModernProtocolErrorsRetryDiscovery() async { jsonEncode( JsonRpcResponse( id: id, - result: const DiscoverResult( - supportedVersions: [ + result: const { + 'resultType': _resultTypeComplete, + 'supportedVersions': [ _draftProtocolVersion2026_07_28, ], - capabilities: ServerCapabilities( - tools: ServerCapabilitiesTools(), - ), - serverInfo: Implementation( - name: 'modern-http-server', - version: '1.0.0', - ), - ).toJson(), + 'capabilities': { + 'tools': {}, + }, + 'serverInfo': { + 'name': 'modern-http-server', + 'version': '1.0.0', + }, + 'ttlMs': 0, + 'cacheScope': _cacheScopePrivate, + }, ).toJson(), ), ); diff --git a/test/interop/ts_2026_rc/.gitignore b/test/interop/ts_2026_rc/.gitignore new file mode 100644 index 00000000..5f77b197 --- /dev/null +++ b/test/interop/ts_2026_rc/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +npm-debug.log* + diff --git a/test/interop/ts_2026_rc/README.md b/test/interop/ts_2026_rc/README.md new file mode 100644 index 00000000..90259249 --- /dev/null +++ b/test/interop/ts_2026_rc/README.md @@ -0,0 +1,35 @@ +# TypeScript SDK 2026 RC Interop + +This fixture is an experimental smoke test for the unreleased MCP +`2026-07-28` draft/RC path against the official TypeScript SDK work in +progress. + +It is intentionally separate from `test/interop/ts`, which tracks the published +stable TypeScript SDK and MCP `2025-11-25` behavior. The published split +TypeScript packages still do not advertise `2026-07-28`, so this fixture pins a +`pkg.pr.new` preview package from TypeScript SDK PR #2327. That PR includes the +modern Streamable HTTP `Mcp-Name` header support needed to interoperate with the +Dart 2026 RC server. + +## Run + +From the repository root: + +```bash +cd test/interop/ts_2026_rc +npm install +cd ../../.. +dart run tool/testing/run_ts_2026_rc_interop.dart +``` + +The runner starts `test/conformance/mcp_2026_rc_server.dart`, waits for its +bound local URL, and then runs `src/client.mjs` against it. The smoke asserts: + +- TypeScript client negotiation selects the modern `2026-07-28` era. +- `tools/list` returns the Dart fixture tools. +- `tools/call` can invoke the Dart `echo` tool over modern Streamable HTTP. + +Keep this as a manual, non-blocking check until the TypeScript SDK publishes a +stable 2026-compatible alpha package or the upstream PR stack lands on the +`v2-2026-07-28` branch. + diff --git a/test/interop/ts_2026_rc/package-lock.json b/test/interop/ts_2026_rc/package-lock.json new file mode 100644 index 00000000..e5667cb7 --- /dev/null +++ b/test/interop/ts_2026_rc/package-lock.json @@ -0,0 +1,148 @@ +{ + "name": "mcp-dart-ts-2026-rc-interop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-dart-ts-2026-rc-interop", + "version": "0.0.0", + "dependencies": { + "@modelcontextprotocol/client": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@87ce6c5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-alpha.2", + "resolved": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@87ce6c5", + "integrity": "sha512-jZ5kzgPjUtC270gykOsntgX/o5v7yGeV46cn2mMvpovxbFDrDvQ08TNRioqbsHl2jKN+HTeXatpnxvDRoQ1+Qw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/test/interop/ts_2026_rc/package.json b/test/interop/ts_2026_rc/package.json new file mode 100644 index 00000000..8d57bc32 --- /dev/null +++ b/test/interop/ts_2026_rc/package.json @@ -0,0 +1,17 @@ +{ + "name": "mcp-dart-ts-2026-rc-interop", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Manual TypeScript SDK 2026 RC interop fixture for mcp_dart.", + "scripts": { + "client": "node src/client.mjs" + }, + "dependencies": { + "@modelcontextprotocol/client": "https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@87ce6c5" + }, + "engines": { + "node": ">=20" + } +} + diff --git a/test/interop/ts_2026_rc/src/client.mjs b/test/interop/ts_2026_rc/src/client.mjs new file mode 100644 index 00000000..3f763b19 --- /dev/null +++ b/test/interop/ts_2026_rc/src/client.mjs @@ -0,0 +1,69 @@ +import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; + +function readArg(args, name) { + const index = args.indexOf(name); + if (index < 0 || index + 1 >= args.length) { + return undefined; + } + return args[index + 1]; +} + +async function main() { + const urlValue = readArg(process.argv.slice(2), '--url'); + if (!urlValue) { + throw new Error('--url is required'); + } + + const client = new Client( + { name: 'mcp-dart-ts-2026-rc-client', version: '0.0.0' }, + { + capabilities: {}, + versionNegotiation: { mode: { pin: '2026-07-28' } }, + }, + ); + const transport = new StreamableHTTPClientTransport(new URL(urlValue)); + + try { + await client.connect(transport); + + const era = client.getProtocolEra(); + const version = client.getNegotiatedProtocolVersion(); + if (era !== 'modern' || version !== '2026-07-28') { + throw new Error(`Expected modern 2026-07-28, got ${era}/${version}`); + } + + const tools = await client.listTools(); + const toolNames = tools.tools.map((tool) => tool.name); + if (!toolNames.includes('echo')) { + throw new Error(`Expected echo tool, got ${toolNames.join(', ')}`); + } + + const message = 'from TypeScript 2026 RC preview'; + const result = await client.callTool({ + name: 'echo', + arguments: { message }, + }); + const content = Array.isArray(result.content) ? result.content : []; + const first = content[0]; + if (!first || first.type !== 'text' || first.text !== message) { + throw new Error(`Unexpected echo result: ${JSON.stringify(result)}`); + } + + console.log( + JSON.stringify({ + protocolEra: era, + protocolVersion: version, + toolCount: toolNames.length, + echo: first.text, + }), + ); + } finally { + await client.close(); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); + diff --git a/test/mcp_2026_07_28_test.dart b/test/mcp_2026_07_28_test.dart index c8fa041e..3b52d017 100644 --- a/test/mcp_2026_07_28_test.dart +++ b/test/mcp_2026_07_28_test.dart @@ -143,6 +143,8 @@ class DiscoveringClientTransport extends Transport supportedVersions: discoverVersions, capabilities: capabilities, serverInfo: const Implementation(name: 'server', version: '1.0.0'), + ttlMs: 0, + cacheScope: CacheScope.private, ).toJson(), ), ); @@ -1029,15 +1031,22 @@ void main() { capabilities: ServerCapabilities(tools: ServerCapabilitiesTools()), serverInfo: Implementation(name: 'server', version: '1.0.0'), instructions: 'Use the tools.', + ttlMs: 1000, + cacheScope: CacheScope.public, ); final resultJson = result.toJson(); expect(resultJson['resultType'], 'complete'); expect(resultJson['supportedVersions'], [draftProtocolVersion2026_07_28]); expect(resultJson['capabilities'], {'tools': {}}); + expect(resultJson['ttlMs'], 1000); + expect(resultJson['cacheScope'], CacheScope.public); + final parsedResult = DiscoverResult.fromJson(resultJson); expect( - DiscoverResult.fromJson(resultJson).instructions, + parsedResult.instructions, 'Use the tools.', ); + expect(parsedResult.ttlMs, 1000); + expect(parsedResult.cacheScope, CacheScope.public); }); test('stateless metadata omits legacy task capabilities', () { @@ -1135,6 +1144,14 @@ void main() { ...result, 'instructions': 1, }), + () => DiscoverResult.fromJson({ + ...result, + 'ttlMs': -1, + }), + () => DiscoverResult.fromJson({ + ...result, + 'cacheScope': 'global', + }), () => ClientCapabilitiesSampling.fromJson({ 'tools': {'bad': Object()}, }), diff --git a/test/server/streamable_mcp_server_test.dart b/test/server/streamable_mcp_server_test.dart index 0fab3953..e8acb735 100644 --- a/test/server/streamable_mcp_server_test.dart +++ b/test/server/streamable_mcp_server_test.dart @@ -558,6 +558,47 @@ void main() { ); }); + test('adds cache hints to stateless server discover responses', () async { + await server.stop(); + server = StreamableMcpServer( + serverFactory: (sessionId) { + return McpServer( + const Implementation(name: 'DiscoverServer', version: '1.0.0'), + options: const McpServerOptions( + protocol: McpProtocol.preview2026, + ), + ); + }, + host: host, + port: port, + enableJsonResponse: true, + ); + await server.start(); + + final response = await http.post( + Uri.parse(baseUrl), + body: jsonEncode({ + 'jsonrpc': jsonRpcVersion, + 'id': 'discover-cache', + 'method': Method.serverDiscover, + 'params': {'_meta': statelessMeta()}, + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/event-stream', + 'MCP-Protocol-Version': draftProtocolVersion2026_07_28, + 'Mcp-Method': Method.serverDiscover, + }, + ); + + expect(response.statusCode, HttpStatus.ok); + final message = jsonDecode(response.body) as Map; + final result = message['result'] as Map; + expect(result['resultType'], resultTypeComplete); + expect(result['ttlMs'], 0); + expect(result['cacheScope'], CacheScope.private); + }); + test('rejects removed stateless request methods before legacy parsing', () async { await server.stop(); diff --git a/tool/testing/run_ts_2026_rc_interop.dart b/tool/testing/run_ts_2026_rc_interop.dart new file mode 100644 index 00000000..a2f83849 --- /dev/null +++ b/tool/testing/run_ts_2026_rc_interop.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +Future main(List args) async { + final repoRoot = Directory.current; + final fixtureDir = Directory('test/interop/ts_2026_rc'); + final clientPackage = File( + 'test/interop/ts_2026_rc/node_modules/' + '@modelcontextprotocol/client/package.json', + ); + + if (!File('pubspec.yaml').existsSync() || !fixtureDir.existsSync()) { + stderr.writeln( + 'Run this command from the mcp_dart repository root.', + ); + exitCode = 64; + return; + } + + if (!clientPackage.existsSync()) { + stderr.writeln( + 'Missing TypeScript fixture dependencies. Run:\n' + ' cd test/interop/ts_2026_rc\n' + ' npm install', + ); + exitCode = 64; + return; + } + + final server = await Process.start( + Platform.resolvedExecutable, + [ + 'run', + 'test/conformance/mcp_2026_rc_server.dart', + '--host', + '127.0.0.1', + '--port', + '0', + ], + workingDirectory: repoRoot.path, + ); + + final serverUrl = Completer(); + final urlPattern = RegExp(r'(http://[^\s]+)'); + + final serverStdout = _pipeLines( + server.stdout, + stdout, + '[dart-server]', + onLine: (line) { + if (serverUrl.isCompleted || + !line.contains('MCP 2026 RC conformance server listening on')) { + return; + } + final match = urlPattern.firstMatch(line); + if (match != null) { + serverUrl.complete(match.group(1)!); + } + }, + ); + final serverStderr = _pipeLines(server.stderr, stderr, '[dart-server]'); + + try { + final url = await serverUrl.future.timeout( + const Duration(seconds: 20), + onTimeout: () { + throw TimeoutException('Timed out waiting for Dart server URL'); + }, + ); + + final client = await Process.start( + 'node', + ['src/client.mjs', '--url', url], + workingDirectory: fixtureDir.path, + ); + final clientStdout = _pipeLines(client.stdout, stdout, '[ts-client]'); + final clientStderr = _pipeLines(client.stderr, stderr, '[ts-client]'); + final clientExit = await client.exitCode.timeout( + const Duration(seconds: 30), + ); + await Future.wait([clientStdout, clientStderr]); + + if (clientExit != 0) { + exitCode = clientExit; + return; + } + } on Object catch (error) { + stderr.writeln('TS 2026 RC interop failed: $error'); + exitCode = 1; + } finally { + await _terminate(server); + await Future.wait([serverStdout, serverStderr]); + } +} + +Future _pipeLines( + Stream> stream, + IOSink sink, + String prefix, { + void Function(String line)? onLine, +}) async { + await for (final line + in stream.transform(utf8.decoder).transform(const LineSplitter())) { + onLine?.call(line); + sink.writeln('$prefix $line'); + } +} + +Future _terminate(Process process) async { + final exitFuture = process.exitCode; + process.kill(ProcessSignal.sigterm); + try { + await exitFuture.timeout(const Duration(seconds: 5)); + } on TimeoutException { + process.kill(ProcessSignal.sigkill); + await exitFuture; + } +}