-
Notifications
You must be signed in to change notification settings - Fork 50
Feat/tasks mrtr extension #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
da57c23
4f2eac0
95da20d
10bf837
b99b877
ac31214
683c633
42b2bb4
e19dbe0
6f0f99f
08c2466
20fc8f4
fba4dfb
f65dd24
56c5d84
29d6501
a93975a
28c0ff1
c37e496
8e4b62c
d24ef6e
f01bf45
9f7cc80
c377ae5
37b7579
fa85a15
6931247
83097b8
a2299fd
3af5a6b
674f4af
6949ec6
40d83be
c76488b
0903f11
fa64304
a943be2
03e4023
32a8e01
6ad5b3a
892bbed
d1e972b
bb88c8f
7cdf10c
ceaf2c4
9c30626
bdebc79
52804e6
157bec3
1069097
c61ed12
9b3c0a5
d7513a1
d062b18
852dd66
4b74f9a
6ead127
be0561a
97ca269
5e6ffc7
62b41cf
a6f7c27
8ebba6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, |
||
| * Test-scaffolding helpers shared across server-conformance scenarios. | ||
| * | ||
| * Pure: no I/O, no async, no scenario-specific state. Every server | ||
| * suite (tasks, mrtr, ...) emits checks in the same JSON shape and | ||
| * derives them through the same FAILURE / SKIPPED helpers; pulling | ||
| * them out of any one suite's helpers.ts makes them reusable. | ||
| * | ||
| * `AnyResult` is the Zod passthrough schema callers used to pair with | ||
| * the official MCP TS SDK's `client.request(req, AnyResult)`. The raw | ||
| * session helpers in `_shared/raw-session.ts` don't depend on Zod, but | ||
| * scenarios that drive the SDK directly (or want to validate a | ||
| * particular result shape later) keep using it. | ||
| */ | ||
|
|
||
| import { z } from 'zod'; | ||
|
|
||
| import type { ConformanceCheck, SpecReference } from '../../../types'; | ||
|
|
||
| export function errMsg(error: unknown): string { | ||
| return error instanceof Error ? error.message : String(error); | ||
| } | ||
|
|
||
| /** Build a FAILURE check from a thrown error, preserving id/name/description. */ | ||
| export function failureCheck( | ||
| id: string, | ||
| name: string, | ||
| description: string, | ||
| error: unknown, | ||
| specReferences: SpecReference[] | ||
| ): ConformanceCheck { | ||
| return { | ||
| id, | ||
| name, | ||
| description, | ||
| status: 'FAILURE', | ||
| timestamp: new Date().toISOString(), | ||
| errorMessage: errMsg(error), | ||
| specReferences | ||
| }; | ||
| } | ||
|
|
||
| /** Build a SKIPPED check (preserves id stability so Ctrl+F still finds it). */ | ||
| export function skipCheck( | ||
| id: string, | ||
| name: string, | ||
| description: string, | ||
| reason: string, | ||
| specReferences: SpecReference[] | ||
| ): ConformanceCheck { | ||
| return { | ||
| id, | ||
| name, | ||
| description, | ||
| status: 'SKIPPED', | ||
| timestamp: new Date().toISOString(), | ||
| errorMessage: `Skipped: ${reason}`, | ||
| specReferences | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Zod passthrough schema. Pair with `client.request(req, AnyResult)` to | ||
| * preserve fields the SDK's typed result schemas would strip. Every | ||
| * SEP-2663 / SEP-2322 wire field falls into this bucket today. | ||
| */ | ||
| export const AnyResult = z.object({}).passthrough(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| /** | ||
| * Unit tests for readJsonRpcResponse — the SSE / JSON content-type | ||
| * dispatcher used by every raw-session request. The SSE branch | ||
| * delegates to `eventsource-parser`; these tests pin the contract | ||
| * across the cases that matter for Streamable HTTP responses: | ||
| * | ||
| * - application/json — single-frame body, id-match required | ||
| * - text/event-stream — multi-event scan, id-match required, | ||
| * reassembly of multi-line `data:` continuations, CRLF tolerance, | ||
| * comment / retry / id field handling, and error-frame return. | ||
| */ | ||
|
|
||
| import { describe, it, expect } from 'vitest'; | ||
|
|
||
| import { readJsonRpcResponse } from './raw-session'; | ||
|
|
||
| function sseResponse(body: string, status = 200): Response { | ||
| return new Response(body, { | ||
| status, | ||
| headers: { 'Content-Type': 'text/event-stream' } | ||
| }); | ||
| } | ||
|
|
||
| function jsonResponse(body: unknown, status = 200): Response { | ||
| return new Response(JSON.stringify(body), { | ||
| status, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
| } | ||
|
|
||
| describe('readJsonRpcResponse', () => { | ||
| describe('application/json branch', () => { | ||
| it('returns the body when id matches', async () => { | ||
| const body = { jsonrpc: '2.0', id: 42, result: { ok: true } }; | ||
| const out = await readJsonRpcResponse(jsonResponse(body), 42); | ||
| expect(out).toEqual(body); | ||
| }); | ||
|
|
||
| it('throws on id mismatch', async () => { | ||
| const body = { jsonrpc: '2.0', id: 99, result: { ok: true } }; | ||
| await expect(readJsonRpcResponse(jsonResponse(body), 42)).rejects.toThrow( | ||
| /JSON-RPC id mismatch.*expected 42.*got 99/ | ||
| ); | ||
| }); | ||
|
|
||
| it('preserves error frames', async () => { | ||
| const body = { | ||
| jsonrpc: '2.0', | ||
| id: 7, | ||
| error: { code: -32601, message: 'Method not found' } | ||
| }; | ||
| const out = await readJsonRpcResponse(jsonResponse(body), 7); | ||
| expect(out.error?.code).toBe(-32601); | ||
| }); | ||
| }); | ||
|
|
||
| describe('text/event-stream branch', () => { | ||
| it('extracts the single JSON-RPC frame from a one-event stream', async () => { | ||
| const frame = { jsonrpc: '2.0', id: 1, result: { ok: true } }; | ||
| const body = `data: ${JSON.stringify(frame)}\n\n`; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 1); | ||
| expect(out).toEqual(frame); | ||
| }); | ||
|
|
||
| it('picks the matching id when earlier events carry other ids', async () => { | ||
| // Streamable HTTP allows in-flight notifications (`notifications/*`, | ||
| // which have no id) followed by the response frame. Make sure we | ||
| // skip the noise and return the response. | ||
| const notif = { | ||
| jsonrpc: '2.0', | ||
| method: 'notifications/progress', | ||
| params: { progress: 50 } | ||
| }; | ||
| const frame = { jsonrpc: '2.0', id: 5, result: { done: true } }; | ||
| const body = | ||
| `data: ${JSON.stringify(notif)}\n\n` + | ||
| `data: ${JSON.stringify(frame)}\n\n`; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 5); | ||
| expect(out).toEqual(frame); | ||
| }); | ||
|
|
||
| it('reassembles multi-line data: continuations', async () => { | ||
| // WHATWG SSE concatenates consecutive `data:` lines with `\n`. | ||
| // The hand-rolled parser this commit replaces also did this, but | ||
| // only because the frames we send happen to fit on one line; pin | ||
| // the multi-line case so a future fixture that emits pretty- | ||
| // printed JSON keeps working. | ||
| const body = | ||
| 'data: {"jsonrpc":"2.0",\ndata: "id":9,\ndata: "result":{}}\n\n'; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 9); | ||
| expect(out).toEqual({ jsonrpc: '2.0', id: 9, result: {} }); | ||
| }); | ||
|
|
||
| it('tolerates CRLF line endings', async () => { | ||
| const frame = { jsonrpc: '2.0', id: 3, result: { ok: true } }; | ||
| const body = `data: ${JSON.stringify(frame)}\r\n\r\n`; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 3); | ||
| expect(out).toEqual(frame); | ||
| }); | ||
|
|
||
| it('skips comment + id + retry fields without misreading them as frames', async () => { | ||
| const frame = { jsonrpc: '2.0', id: 11, result: { ok: true } }; | ||
| const body = | ||
| ': heartbeat\n' + | ||
| 'retry: 5000\n' + | ||
| 'id: event-7\n' + | ||
| '\n' + | ||
| `event: message\ndata: ${JSON.stringify(frame)}\n\n`; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 11); | ||
| expect(out).toEqual(frame); | ||
| }); | ||
|
|
||
| it('returns error frames from the SSE stream', async () => { | ||
| const frame = { | ||
| jsonrpc: '2.0', | ||
| id: 4, | ||
| error: { code: -32001, message: 'HeaderMismatch' } | ||
| }; | ||
| const body = `data: ${JSON.stringify(frame)}\n\n`; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 4); | ||
| expect(out.error?.code).toBe(-32001); | ||
| }); | ||
|
|
||
| it('throws when no event matches the expected id', async () => { | ||
| // Only non-response events on the stream. | ||
| const notif = { | ||
| jsonrpc: '2.0', | ||
| method: 'notifications/progress', | ||
| params: { progress: 100 } | ||
| }; | ||
| const body = `data: ${JSON.stringify(notif)}\n\n`; | ||
| await expect(readJsonRpcResponse(sseResponse(body), 7)).rejects.toThrow( | ||
| /No JSON-RPC frame with id=7/ | ||
| ); | ||
| }); | ||
|
|
||
| it('throws on empty stream body', async () => { | ||
| await expect(readJsonRpcResponse(sseResponse(''), 1)).rejects.toThrow( | ||
| /No JSON-RPC frame with id=1/ | ||
| ); | ||
| }); | ||
|
|
||
| it('skips events with non-JSON data without throwing', async () => { | ||
| // Plain text events shouldn't blow up the parser; we just skip | ||
| // them and keep scanning for the JSON-RPC frame. | ||
| const frame = { jsonrpc: '2.0', id: 8, result: { ok: true } }; | ||
| const body = 'data: keepalive\n\n' + `data: ${JSON.stringify(frame)}\n\n`; | ||
| const out = await readJsonRpcResponse(sseResponse(body), 8); | ||
| expect(out).toEqual(frame); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm can you explain this? I don't think this should be necessary, i.e. it should be implied by the spec version
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. The field was added to drive a (wire × scenario) matrix back when both wires were spec-valid on the draft. Since SEP-2575 removed the legacy
initializeonDRAFT-2026-v1collapsing that to a single col and turning the field into a way for callers to go out of sync withspecVersion.Dropped it - wire now derives from
specVersionviaisStateless(ctx)inconnection/select.ts. Follow-up PR: panyam#8.