Skip to content

Commit 42b2bb4

Browse files
committed
tasks: align ttlMs / pollIntervalMs assertions per 2026-05-07 SEP
PR 2663 commit 62758914 standardised every duration field on the Ms suffix, integer milliseconds. wire-fields.ts now asserts ttlMs and pollIntervalMs are present on CreateTaskResult, the legacy v1 ttl and pollInterval keys are absent (already covered), and the interim ttlSeconds / pollIntervalMilliseconds keys are also absent on a post-2026-05-07 server. lifecycle.ts and the scenario README pick up matching prose updates. Verified by make testconf-tasks-v2 (8/8) against a renamed mcpkit fixture, and make testconf-mrtr (7/7 + 1 SKIPPED) against the paired MRTR surface.
1 parent 683c633 commit 42b2bb4

3 files changed

Lines changed: 60 additions & 39 deletions

File tree

src/scenarios/server/tasks/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ aren't stripped.
1616

1717
| SEP | What it adds | Where it shows up |
1818
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
19-
| SEP-2663 | Tasks Extension — `io.modelcontextprotocol/tasks` capability, flat `CreateTaskResult` (`Result & Task`), `DetailedTask` on `tasks/get` (with inlined result/error/inputRequests/requestState), `tasks/update` for MRTR resume, ack-only `tasks/cancel`, wire-field renames (`ttlSeconds`, `pollIntervalMilliseconds`) | every scenario |
19+
| SEP-2663 | Tasks Extension — `io.modelcontextprotocol/tasks` capability, flat `CreateTaskResult` (`Result & Task`), `DetailedTask` on `tasks/get` (with inlined result/error/inputRequests/requestState), `tasks/update` for MRTR resume, ack-only `tasks/cancel`, wire-field renames (`ttlMs`, `pollIntervalMs`, both integer milliseconds per the 2026-05-07 spec commit aligning duration suffixes) | every scenario |
2020
| SEP-2322 | MRTR base types — `inputRequests`/`inputResponses` keyed maps, `requestState`, `resultType` discriminator (`"task"`/`"complete"`/`"incomplete"`) | request-state, mrtr-input, dispatch |
2121
| SEP-2575 | Per-request capability override via `_meta.io.modelcontextprotocol/clientCapabilities` | capability |
2222
| SEP-2243 | Server tolerates `Mcp-Method` / `Mcp-Name` request headers as informational routing metadata; body is authoritative | headers |
@@ -56,8 +56,8 @@ vs protocol errors, cancellation semantics.
5656

5757
| Check | What it tests |
5858
| ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
59-
| `tasks-wire-field-renames` | `ttlSeconds` + `pollIntervalMilliseconds` present; legacy `ttl` / `pollInterval` keys absent |
60-
| `tasks-no-early-ttl-expiry` | Task remains accessible via `tasks/get` for the duration of its `ttlSeconds` |
59+
| `tasks-wire-field-renames` | `ttlMs` + `pollIntervalMs` present; legacy `ttl` / `pollInterval` and interim `ttlSeconds` / `pollIntervalMilliseconds` keys absent |
60+
| `tasks-no-early-ttl-expiry` | Task remains accessible via `tasks/get` for the duration of its `ttlMs` |
6161
| `tasks-no-related-task-meta-on-inlined-result` | v1 `io.modelcontextprotocol/related-task` `_meta` key absent on tasks/get's inlined `result` |
6262

6363
### `tasks-request-state` (`request-state.ts`)
@@ -166,14 +166,14 @@ server fails loudly rather than appearing well-formed. Today:
166166
| Client opt-in | (none) | MUST declare extension at session OR per-request (SEP-2575) |
167167
| Task creation | Client sends `task` hint param | Server decides unilaterally |
168168
| `resultType` discriminator | absent | `"task"` (CreateTaskResult) / `"complete"` (everything else) / `"incomplete"` (MRTR ephemeral) |
169-
| `CreateTaskResult` shape | `{task: {...}}` (nested) | flat: `{resultType, taskId, status, ttlSeconds, ...}` (no nested wrapper) |
169+
| `CreateTaskResult` shape | `{task: {...}}` (nested) | flat: `{resultType, taskId, status, ttlMs, ...}` (no nested wrapper) |
170170
| `tasks/get` response | flat `TaskInfo` only | `DetailedTask` with inlined `result`/`error`/`inputRequests`/`requestState` |
171171
| `tasks/update` | n/a | new — MRTR resume path, returns `{resultType:"complete"}` ack |
172172
| `tasks/cancel` response | rich task envelope | `{resultType:"complete"}` ack (no task state) |
173173
| `tasks/result` | separate blocking method | **removed** (result inlined on `tasks/get`) |
174174
| `tasks/list` | session-scoped list | **removed** |
175-
| TTL field | `ttl` (ms by convention) | `ttlSeconds` (units in name) |
176-
| Poll-interval field | `pollInterval` | `pollIntervalMilliseconds` |
175+
| TTL field | `ttl` (ms by convention) | `ttlMs` (integer milliseconds, units in name) |
176+
| Poll-interval field | `pollInterval` | `pollIntervalMs` (integer milliseconds) |
177177
| `parentTaskId` | present | removed |
178178
| Tool errors | `status:failed` | `status:completed, result.isError:true` |
179179
| Mcp-Name HTTP header | not set | request-side routing header (SEP-2243) |

src/scenarios/server/tasks/lifecycle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The server MUST advertise \`io.modelcontextprotocol/tasks\` under
5858
the client MUST NOT need to opt in via a request param.
5959
- The response MUST be a \`CreateTaskResult\` — a flat \`Result & Task\`
6060
intersection: \`resultType:"task"\`, plus \`taskId\` / \`status\` /
61-
\`createdAt\` / \`lastUpdatedAt\` / \`ttlSeconds\` at the top level.
61+
\`createdAt\` / \`lastUpdatedAt\` / \`ttlMs\` at the top level.
6262
There MUST NOT be a nested \`task\` wrapper key.
6363
6464
**tasks/get DetailedTask:**

src/scenarios/server/tasks/wire-fields.ts

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* SEP-2663 Tasks Extension — wire-format / TTL conformance.
33
*
4-
* Tests the renamed wire fields (ttlSeconds, pollIntervalMilliseconds),
4+
* Tests the renamed wire fields (ttlMs, pollIntervalMs),
55
* the no-early-TTL-expiry rule, and confirms the v1 `related-task` _meta
66
* key is absent on tasks/get's inlined result (taskId is at root level
77
* already, so the metadata is redundant).
@@ -37,18 +37,23 @@ export class TasksWireFieldsScenario implements ClientScenario {
3737
**Server Implementation Requirements:**
3838
3939
**Wire-field renames (SEP-2663):**
40-
- The TTL field is named \`ttlSeconds\` on the wire (the v1 \`ttl\`
41-
key is in milliseconds-by-convention; SEP-2663 puts the unit in the
42-
field name).
43-
- The poll-interval field is named \`pollIntervalMilliseconds\` (v1
44-
used \`pollInterval\`).
40+
- The TTL field is named \`ttlMs\` on the wire (the v1 \`ttl\` key was
41+
in milliseconds-by-convention; SEP-2663 puts the unit in the field
42+
name and standardised on the \`Ms\` suffix in the 2026-05-07 spec
43+
commit aligning all duration fields).
44+
- The poll-interval field is named \`pollIntervalMs\` (v1 used
45+
\`pollInterval\`; an interim SEP-2663 draft used
46+
\`pollIntervalMilliseconds\` before the 2026-05-07 \`Ms\`-suffix
47+
alignment).
4548
- A \`CreateTaskResult\` MUST NOT carry the legacy \`ttl\` or
4649
\`pollInterval\` keys — clients keying off v1 names on a v2 server
47-
would silently miss the TTL guidance.
50+
would silently miss the TTL guidance. The interim
51+
\`ttlSeconds\` / \`pollIntervalMilliseconds\` keys MUST also be
52+
absent on a server tracking the post-2026-05-07 spec.
4853
4954
**TTL non-expiry (SEP-2663):**
5055
- A task MUST remain accessible via \`tasks/get\` for the duration of
51-
its \`ttlSeconds\`; a server MUST NOT expire it earlier.
56+
its \`ttlMs\`; a server MUST NOT expire it earlier.
5257
5358
**Inlined-result \`_meta\` (SEP-2663):**
5459
- The v1 \`io.modelcontextprotocol/related-task\` \`_meta\` key MUST NOT
@@ -86,13 +91,13 @@ export class TasksWireFieldsScenario implements ClientScenario {
8691
return checks;
8792
}
8893

89-
// Check 1: ttlSeconds + pollIntervalMilliseconds wire shape.
94+
// Check 1: ttlMs + pollIntervalMs wire shape.
9095
let createdTaskId: string | undefined;
9196
{
9297
const id = 'tasks-wire-field-renames';
9398
const name = 'TasksWireFieldRenames';
9499
const description =
95-
'CreateTaskResult uses ttlSeconds + pollIntervalMilliseconds; legacy ttl / pollInterval keys absent';
100+
'CreateTaskResult uses ttlMs + pollIntervalMs; legacy ttl / pollInterval keys absent and the interim ttlSeconds / pollIntervalMilliseconds keys also absent';
96101
try {
97102
const result = (await client.request(
98103
{
@@ -106,40 +111,52 @@ export class TasksWireFieldsScenario implements ClientScenario {
106111
)) as any;
107112
createdTaskId = result.taskId;
108113
const errs: string[] = [];
109-
// ttlSeconds — required, positive (or null = unlimited; treat
110-
// either as well-formed). Legacy `ttl` MUST be absent.
111-
if (!('ttlSeconds' in result)) {
114+
// ttlMs — required, positive integer (or null = unlimited; treat
115+
// either as well-formed). Legacy `ttl` MUST be absent. Interim
116+
// `ttlSeconds` MUST also be absent on a post-2026-05-07 server.
117+
if (!('ttlMs' in result)) {
112118
errs.push(
113-
'CreateTaskResult MUST carry ttlSeconds (renamed from v1 `ttl`)'
119+
'CreateTaskResult MUST carry ttlMs (renamed from v1 `ttl` and from the interim `ttlSeconds`)'
114120
);
115121
} else if (
116-
result.ttlSeconds !== null &&
117-
(typeof result.ttlSeconds !== 'number' || result.ttlSeconds <= 0)
122+
result.ttlMs !== null &&
123+
(typeof result.ttlMs !== 'number' || result.ttlMs <= 0)
118124
) {
119125
errs.push(
120-
`ttlSeconds MUST be null or a positive number; got ${JSON.stringify(result.ttlSeconds)}`
126+
`ttlMs MUST be null or a positive number (integer milliseconds); got ${JSON.stringify(result.ttlMs)}`
121127
);
122128
}
123129
if ('ttl' in result) {
124130
errs.push(
125-
'CreateTaskResult MUST NOT carry the v1 `ttl` key (use ttlSeconds)'
131+
'CreateTaskResult MUST NOT carry the v1 `ttl` key (use ttlMs)'
126132
);
127133
}
128-
// pollIntervalMilliseconds — optional. When present it MUST be
129-
// a positive number and the legacy `pollInterval` key MUST NOT
130-
// appear.
134+
if ('ttlSeconds' in result) {
135+
errs.push(
136+
'CreateTaskResult MUST NOT carry the interim `ttlSeconds` key (the 2026-05-07 SEP-2663 commit replaced it with ttlMs)'
137+
);
138+
}
139+
// pollIntervalMs — optional. When present it MUST be a positive
140+
// integer (milliseconds), and the legacy `pollInterval` key MUST
141+
// NOT appear. The interim `pollIntervalMilliseconds` key MUST
142+
// also be absent on a post-2026-05-07 server.
131143
if (
132-
result.pollIntervalMilliseconds !== undefined &&
133-
(typeof result.pollIntervalMilliseconds !== 'number' ||
134-
result.pollIntervalMilliseconds <= 0)
144+
result.pollIntervalMs !== undefined &&
145+
(typeof result.pollIntervalMs !== 'number' ||
146+
result.pollIntervalMs <= 0)
135147
) {
136148
errs.push(
137-
`pollIntervalMilliseconds MUST be a positive number when present; got ${JSON.stringify(result.pollIntervalMilliseconds)}`
149+
`pollIntervalMs MUST be a positive number when present; got ${JSON.stringify(result.pollIntervalMs)}`
138150
);
139151
}
140152
if ('pollInterval' in result) {
141153
errs.push(
142-
'CreateTaskResult MUST NOT carry the v1 `pollInterval` key (use pollIntervalMilliseconds)'
154+
'CreateTaskResult MUST NOT carry the v1 `pollInterval` key (use pollIntervalMs)'
155+
);
156+
}
157+
if ('pollIntervalMilliseconds' in result) {
158+
errs.push(
159+
'CreateTaskResult MUST NOT carry the interim `pollIntervalMilliseconds` key (the 2026-05-07 SEP-2663 commit replaced it with pollIntervalMs)'
143160
);
144161
}
145162
checks.push({
@@ -151,10 +168,13 @@ export class TasksWireFieldsScenario implements ClientScenario {
151168
errorMessage: errs.length > 0 ? errs.join('; ') : undefined,
152169
specReferences: [SEP_2663_REF],
153170
details: {
154-
ttlSeconds: result.ttlSeconds,
155-
pollIntervalMilliseconds: result.pollIntervalMilliseconds,
171+
ttlMs: result.ttlMs,
172+
pollIntervalMs: result.pollIntervalMs,
156173
hasLegacyTtl: 'ttl' in result,
157-
hasLegacyPollInterval: 'pollInterval' in result
174+
hasLegacyPollInterval: 'pollInterval' in result,
175+
hasInterimTtlSeconds: 'ttlSeconds' in result,
176+
hasInterimPollIntervalMilliseconds:
177+
'pollIntervalMilliseconds' in result
158178
}
159179
});
160180
} catch (error) {
@@ -167,14 +187,15 @@ export class TasksWireFieldsScenario implements ClientScenario {
167187
const id = 'tasks-no-early-ttl-expiry';
168188
const name = 'TasksNoEarlyTtlExpiry';
169189
const description =
170-
'Task remains accessible via tasks/get for the duration of its ttlSeconds';
190+
'Task remains accessible via tasks/get for the duration of its ttlMs';
171191
if (!createdTaskId) {
172192
checks.push(skipCheck(id, name, description, 'no task created'));
173193
} else {
174194
try {
175195
await waitForTerminal(client, createdTaskId);
176-
// Sanity probe well before TTL (the unit is seconds; servers
177-
// typically pick order-of-minutes defaults).
196+
// Sanity probe well before TTL elapses. ttlMs is integer
197+
// milliseconds and servers typically pick order-of-minutes
198+
// defaults, so a 500ms wait is comfortably inside any sane TTL.
178199
await new Promise((r) => setTimeout(r, 500));
179200
const after = (await client.request(
180201
{

0 commit comments

Comments
 (0)