@fleetless/contracts 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ the wire shapes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] — 2026-09-18
11
+
12
+ ### Added
13
+
14
+ - **The MCP token request has its refresh grant back.** `oauthTokenRequest` is a discriminated union again: `oauthCodeTokenRequest` (unchanged) or the new `oauthRefreshTokenRequest` — `grant_type: refresh_token`, `refresh_token`, a required `client_id` and an optional RFC 8707 `resource`. Both MCP authorization servers answer it from cloud 0.20.0: every exchange issues a refresh token, every refresh rotates it, and it lives ninety days from its last use. The registration, token-response and metadata descriptions and the four route notes stop promising there is no refresh grant. Nothing previously valid becomes invalid.
15
+
16
+ ## [1.1.0] — 2026-09-17
17
+
18
+ ### Added
19
+
20
+ - **Two discovery routes for app users**, the REST twins of the MCP tools every session starts from: `GET /api/client/robots` lists the robots the caller reaches (`clientRobotListResponse`, new), and `GET /api/robots/:id/datasheet` answers the same `mcpRobotDatasheet` that `robot_describe` does — every granted slug with its kind, unit, decimals and parameter JSON Schema, plus the `action_history` and `assets` capabilities. No existing wire shape changes.
21
+
10
22
  ## [1.0.6] — 2026-09-16
11
23
 
12
24
  - Published from GitHub Actions by npm trusted publishing: no publish token exists anywhere, and every version from this one on carries a provenance attestation linking it to the commit and the run that built it. `npm audit signatures` checks it.
@@ -3382,7 +3382,7 @@
3382
3382
  }
3383
3383
  }
3384
3384
  },
3385
- "description": "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because §3.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which §3.2.1 allows a server to substitute — this authorization server issues `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`.",
3385
+ "description": "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because §3.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which §3.2.1 allows a server to substitute — this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`.",
3386
3386
  "requestBody": {
3387
3387
  "required": true,
3388
3388
  "content": {
@@ -3522,7 +3522,7 @@
3522
3522
  }
3523
3523
  }
3524
3524
  },
3525
- "description": "Only `authorization_code` is supported — there is no refresh grant here, so a session ends when its token expires and the client signs in again. Refusals are RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference. The response carries no `refresh_token`; the shape is the same `oauthTokenResponse` the app flow answers, whose refresh field is optional. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for.",
3525
+ "description": "`authorization_code` mints an `mcp_session` access token bound to the central resource and a refresh token; `refresh_token` rotates that pair, and the presented refresh token is consumed — a second presentation revokes the session, as on `/api/auth/refresh`. The refresh token lives ninety days from its last use and is bound to the `client_id` it was issued to. A refresh re-reads the Fleetless user, so a removed account cannot refresh. Refusals are RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for; a `resource` on a refresh must match the session's audience, and is checked before the token is consumed.",
3526
3526
  "requestBody": {
3527
3527
  "required": true,
3528
3528
  "content": {
@@ -3814,7 +3814,7 @@
3814
3814
  }
3815
3815
  }
3816
3816
  },
3817
- "description": "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which §3.2.1 allows — `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from — a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not.",
3817
+ "description": "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which §3.2.1 allows — this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from — a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not.",
3818
3818
  "requestBody": {
3819
3819
  "required": true,
3820
3820
  "content": {
@@ -3973,7 +3973,7 @@
3973
3973
  }
3974
3974
  }
3975
3975
  },
3976
- "description": "Only `authorization_code`, PKCE-verified and single-use. There is no refresh grant here either, so a session ends when its token expires and the client signs in again; the shape is the same `oauthTokenResponse` the central endpoint answers, whose refresh field is optional and stays empty. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it — that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash.",
3976
+ "description": "`authorization_code`, PKCE-verified and single-use, and `refresh_token`, which rotates the pair the exchange minted; the refresh token lives ninety days from its last use, is bound to its client and to this app, and a refresh re-reads the app user's status and their standing consent to the client, so a block or a withdrawn consent ends the session at its next refresh at the latest. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it — that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash.",
3977
3977
  "requestBody": {
3978
3978
  "required": true,
3979
3979
  "content": {
@@ -5446,6 +5446,104 @@
5446
5446
  "description": "For a client caller the grant check runs **before** any existence lookup, with no extra query on either path to time: a denied slug and a nonexistent one must be one answer. That is why an ungranted slug is `403 forbidden` while a granted-but-unconfigured one is `404 unknown_datapoint` and a configured one with no sample yet is `404 no_data` — three facts a caller who is entitled to them needs told apart. The plane built-ins (`bridge_state`, `robot_details`) answer here too, without appearing in any document."
5447
5447
  }
5448
5448
  },
5449
+ "/api/client/robots": {
5450
+ "get": {
5451
+ "operationId": "get_api_client_robots",
5452
+ "summary": "Lists the robots the caller reaches, with bridge state and the published configuration version.",
5453
+ "tags": [
5454
+ "robots"
5455
+ ],
5456
+ "security": [
5457
+ {
5458
+ "developerSession": []
5459
+ },
5460
+ {
5461
+ "clientToken": []
5462
+ },
5463
+ {
5464
+ "serverKey": []
5465
+ }
5466
+ ],
5467
+ "parameters": [],
5468
+ "responses": {
5469
+ "200": {
5470
+ "description": "Success.",
5471
+ "content": {
5472
+ "application/json": {
5473
+ "schema": {
5474
+ "$ref": "#/components/schemas/client-robot-list-response"
5475
+ }
5476
+ }
5477
+ }
5478
+ },
5479
+ "default": {
5480
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `forbidden`.",
5481
+ "content": {
5482
+ "application/json": {
5483
+ "schema": {
5484
+ "$ref": "#/components/schemas/api-error"
5485
+ }
5486
+ }
5487
+ }
5488
+ }
5489
+ },
5490
+ "description": "**The REST twin of the MCP tool `robots_list`**, and the one robot question no robot-scoped route can answer: which robots may I name at all. An app user sees the robots their app attaches on which their role grants at least one slug or capability; a server key sees every robot its app attaches; a developer bearer sees the organisation's robots. Name order, id as the tiebreak. A robot on which the role grants nothing is absent rather than listed empty — the same answer `robots_list` gives, for the same reason: reach is a grant, not an attachment. Under `/api/client/` because it names no robot; every robot-scoped read stays under `/api/robots/:id/…`."
5491
+ }
5492
+ },
5493
+ "/api/robots/{id}/datasheet": {
5494
+ "get": {
5495
+ "operationId": "get_api_robots_id_datasheet",
5496
+ "summary": "Describes everything the caller's role lets them do on one robot, with parameter schemas.",
5497
+ "tags": [
5498
+ "robots"
5499
+ ],
5500
+ "security": [
5501
+ {
5502
+ "developerSession": []
5503
+ },
5504
+ {
5505
+ "clientToken": []
5506
+ },
5507
+ {
5508
+ "serverKey": []
5509
+ }
5510
+ ],
5511
+ "parameters": [
5512
+ {
5513
+ "name": "id",
5514
+ "in": "path",
5515
+ "required": true,
5516
+ "description": "The robot's uuid, as `GET /api/client/robots` lists it.",
5517
+ "schema": {
5518
+ "type": "string"
5519
+ }
5520
+ }
5521
+ ],
5522
+ "responses": {
5523
+ "200": {
5524
+ "description": "Success.",
5525
+ "content": {
5526
+ "application/json": {
5527
+ "schema": {
5528
+ "$ref": "#/components/schemas/mcp-robot-datasheet"
5529
+ }
5530
+ }
5531
+ }
5532
+ },
5533
+ "default": {
5534
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `forbidden`, `invalid_uuid`, `not_found`.",
5535
+ "content": {
5536
+ "application/json": {
5537
+ "schema": {
5538
+ "$ref": "#/components/schemas/api-error"
5539
+ }
5540
+ }
5541
+ }
5542
+ }
5543
+ },
5544
+ "description": "**The REST twin of the MCP tool `robot_describe`**: one answer per robot — every datapoint, action, service, publisher and camera the role grants, each with its `input_schema` where it takes parameters, plus the two capabilities that gate whole features, `action_history` and `assets`. A robot with nothing published answers an empty `exposures` list, never a refusal. A robot the caller does not reach — not attached to their app, or attached with a role that grants nothing on it — answers `404` exactly as one that does not exist. The app-user datapoint and camera listings under this prefix stay; this is the one read that also names actions, services, publishers and capabilities, which is what an app needs before it can draw a screen."
5545
+ }
5546
+ },
5449
5547
  "/api/robots/{id}/config/draft": {
5450
5548
  "get": {
5451
5549
  "operationId": "get_api_robots_id_config_draft",
@@ -9855,7 +9953,7 @@
9855
9953
  "refresh_token"
9856
9954
  ]
9857
9955
  },
9858
- "description": "The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here."
9956
+ "description": "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
9859
9957
  },
9860
9958
  "code_challenge_methods_supported": {
9861
9959
  "type": "array",
@@ -10395,6 +10493,88 @@
10395
10493
  ],
10396
10494
  "additionalProperties": false
10397
10495
  },
10496
+ "client-robot-list-response": {
10497
+ "type": "object",
10498
+ "properties": {
10499
+ "robots": {
10500
+ "type": "array",
10501
+ "items": {
10502
+ "type": "object",
10503
+ "properties": {
10504
+ "id": {
10505
+ "type": "string",
10506
+ "format": "uuid",
10507
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
10508
+ "description": "The robot, and what every robot-scoped route takes as its `:id`."
10509
+ },
10510
+ "name": {
10511
+ "type": "string",
10512
+ "minLength": 1,
10513
+ "maxLength": 63,
10514
+ "description": "The robot's display name, at most 63 characters. Free text, changed through `PATCH /api/robots/:id`."
10515
+ },
10516
+ "created_at": {
10517
+ "type": "string",
10518
+ "format": "date-time",
10519
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
10520
+ "description": "When the robot was created, as an ISO 8601 timestamp."
10521
+ },
10522
+ "bridge_state": {
10523
+ "type": "object",
10524
+ "properties": {
10525
+ "online": {
10526
+ "type": "boolean"
10527
+ },
10528
+ "latency_ms": {
10529
+ "anyOf": [
10530
+ {
10531
+ "type": "number",
10532
+ "minimum": 0
10533
+ },
10534
+ {
10535
+ "type": "null"
10536
+ }
10537
+ ]
10538
+ }
10539
+ },
10540
+ "required": [
10541
+ "online",
10542
+ "latency_ms"
10543
+ ],
10544
+ "additionalProperties": false,
10545
+ "description": "The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is."
10546
+ },
10547
+ "published_version": {
10548
+ "anyOf": [
10549
+ {
10550
+ "type": "integer",
10551
+ "exclusiveMinimum": 0,
10552
+ "maximum": 9007199254740991
10553
+ },
10554
+ {
10555
+ "type": "null"
10556
+ }
10557
+ ],
10558
+ "description": "The published configuration version, or `null` when nothing has been published yet. A robot with nothing published is still listed — \"not configured yet\" is a real state, and the caller is entitled to it — and its datasheet answers an empty exposure list."
10559
+ }
10560
+ },
10561
+ "required": [
10562
+ "id",
10563
+ "name",
10564
+ "created_at",
10565
+ "bridge_state",
10566
+ "published_version"
10567
+ ],
10568
+ "additionalProperties": false
10569
+ },
10570
+ "description": "Every robot the caller reaches, in name order with the id as the tiebreak. An app user reaches the robots their app attaches on which their role grants at least one slug or capability; a server key reaches every robot its app attaches; a developer reaches every robot of the organisation."
10571
+ }
10572
+ },
10573
+ "required": [
10574
+ "robots"
10575
+ ],
10576
+ "additionalProperties": false
10577
+ },
10398
10578
  "client-verify-email-request": {
10399
10579
  "type": "object",
10400
10580
  "properties": {
@@ -13020,7 +13200,7 @@
13020
13200
  ]
13021
13201
  },
13022
13202
  "grant_types": {
13023
- "description": "Accepted for conformance with RFC 7591 and then **ignored**. What comes back is what was actually granted, which §3.2.1 permits a server to substitute: `authorization_code` and nothing else, so a client that asks for `refresh_token` is registered and told plainly that it did not get one.",
13203
+ "description": "Accepted for conformance with RFC 7591 and then **ignored**: both MCP authorization servers grant `authorization_code` and `refresh_token` to every registration, and the answer states what was granted (§3.2.1) rather than what was asked.",
13024
13204
  "type": "array",
13025
13205
  "items": {
13026
13206
  "type": "string",
@@ -13080,7 +13260,7 @@
13080
13260
  "items": {
13081
13261
  "type": "string"
13082
13262
  },
13083
- "description": "The grants this client may use. Always exactly `[\"authorization_code\"]` — a client that asked for `refresh_token` is registered and told here that it did not get one, which is the substitution RFC 7591 §3.2.1 permits."
13263
+ "description": "The grants this client may use. Always exactly `[\"authorization_code\", \"refresh_token\"]` — an exchange mints a refresh token and the token endpoint rotates it."
13084
13264
  },
13085
13265
  "response_types": {
13086
13266
  "type": "array",
@@ -14446,6 +14626,120 @@
14446
14626
  ],
14447
14627
  "additionalProperties": false
14448
14628
  },
14629
+ "mcp-robot-datasheet": {
14630
+ "type": "object",
14631
+ "properties": {
14632
+ "robot_id": {
14633
+ "type": "string",
14634
+ "format": "uuid",
14635
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
14636
+ },
14637
+ "robot_name": {
14638
+ "type": "string",
14639
+ "minLength": 1,
14640
+ "maxLength": 200
14641
+ },
14642
+ "capabilities": {
14643
+ "type": "object",
14644
+ "properties": {
14645
+ "action_history": {
14646
+ "type": "boolean"
14647
+ },
14648
+ "assets": {
14649
+ "type": "boolean"
14650
+ }
14651
+ },
14652
+ "required": [
14653
+ "action_history",
14654
+ "assets"
14655
+ ],
14656
+ "additionalProperties": false
14657
+ },
14658
+ "exposures": {
14659
+ "maxItems": 2000,
14660
+ "type": "array",
14661
+ "items": {
14662
+ "type": "object",
14663
+ "properties": {
14664
+ "slug": {
14665
+ "type": "string",
14666
+ "minLength": 2,
14667
+ "maxLength": 63,
14668
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
14669
+ },
14670
+ "kind": {
14671
+ "type": "string",
14672
+ "enum": [
14673
+ "datapoint",
14674
+ "service",
14675
+ "action",
14676
+ "publisher",
14677
+ "camera"
14678
+ ]
14679
+ },
14680
+ "description": {
14681
+ "anyOf": [
14682
+ {
14683
+ "type": "string",
14684
+ "maxLength": 2000
14685
+ },
14686
+ {
14687
+ "type": "null"
14688
+ }
14689
+ ]
14690
+ },
14691
+ "unit": {
14692
+ "anyOf": [
14693
+ {
14694
+ "type": "string",
14695
+ "maxLength": 32
14696
+ },
14697
+ {
14698
+ "type": "null"
14699
+ }
14700
+ ]
14701
+ },
14702
+ "decimals": {
14703
+ "anyOf": [
14704
+ {
14705
+ "type": "integer",
14706
+ "minimum": 0,
14707
+ "maximum": 6
14708
+ },
14709
+ {
14710
+ "type": "null"
14711
+ }
14712
+ ]
14713
+ },
14714
+ "input_schema": {
14715
+ "anyOf": [
14716
+ {},
14717
+ {
14718
+ "type": "null"
14719
+ }
14720
+ ]
14721
+ }
14722
+ },
14723
+ "required": [
14724
+ "slug",
14725
+ "kind",
14726
+ "description",
14727
+ "unit",
14728
+ "decimals",
14729
+ "input_schema"
14730
+ ],
14731
+ "additionalProperties": false
14732
+ }
14733
+ }
14734
+ },
14735
+ "required": [
14736
+ "robot_id",
14737
+ "robot_name",
14738
+ "capabilities",
14739
+ "exposures"
14740
+ ],
14741
+ "additionalProperties": false
14742
+ },
14449
14743
  "mcp-role-preview-response": {
14450
14744
  "type": "object",
14451
14745
  "properties": {
@@ -14580,50 +14874,88 @@
14580
14874
  "additionalProperties": false
14581
14875
  },
14582
14876
  "oauth-token-request": {
14583
- "type": "object",
14584
- "properties": {
14585
- "grant_type": {
14586
- "type": "string",
14587
- "const": "authorization_code",
14588
- "description": "Always `authorization_code`: this request exchanges the code from the authorize redirect for tokens. Any other value — `refresh_token` included — is `unsupported_grant_type`, refused before the code is looked up."
14589
- },
14590
- "code": {
14591
- "type": "string",
14592
- "minLength": 1,
14593
- "maxLength": 500,
14594
- "description": "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
14595
- },
14596
- "redirect_uri": {
14597
- "type": "string",
14598
- "minLength": 1,
14599
- "maxLength": 2000,
14600
- "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
14601
- },
14602
- "client_id": {
14603
- "type": "string",
14604
- "minLength": 1,
14605
- "maxLength": 200,
14606
- "description": "The client making the exchange, as registered."
14607
- },
14608
- "code_verifier": {
14609
- "type": "string",
14610
- "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
14611
- "description": "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
14877
+ "oneOf": [
14878
+ {
14879
+ "type": "object",
14880
+ "properties": {
14881
+ "grant_type": {
14882
+ "type": "string",
14883
+ "const": "authorization_code",
14884
+ "description": "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
14885
+ },
14886
+ "code": {
14887
+ "type": "string",
14888
+ "minLength": 1,
14889
+ "maxLength": 500,
14890
+ "description": "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
14891
+ },
14892
+ "redirect_uri": {
14893
+ "type": "string",
14894
+ "minLength": 1,
14895
+ "maxLength": 2000,
14896
+ "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
14897
+ },
14898
+ "client_id": {
14899
+ "type": "string",
14900
+ "minLength": 1,
14901
+ "maxLength": 200,
14902
+ "description": "The client making the exchange, as registered."
14903
+ },
14904
+ "code_verifier": {
14905
+ "type": "string",
14906
+ "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
14907
+ "description": "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
14908
+ },
14909
+ "resource": {
14910
+ "description": "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app's endpoint.",
14911
+ "type": "string",
14912
+ "format": "uri"
14913
+ }
14914
+ },
14915
+ "required": [
14916
+ "grant_type",
14917
+ "code",
14918
+ "redirect_uri",
14919
+ "client_id",
14920
+ "code_verifier"
14921
+ ],
14922
+ "description": "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too."
14612
14923
  },
14613
- "resource": {
14614
- "description": "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app's endpoint.",
14615
- "type": "string",
14616
- "format": "uri"
14924
+ {
14925
+ "type": "object",
14926
+ "properties": {
14927
+ "grant_type": {
14928
+ "type": "string",
14929
+ "const": "refresh_token",
14930
+ "description": "`refresh_token`: this request rotates a refresh token into a new access token and a new refresh token. The presented token is consumed; presenting it again revokes the whole session."
14931
+ },
14932
+ "refresh_token": {
14933
+ "type": "string",
14934
+ "minLength": 1,
14935
+ "maxLength": 500,
14936
+ "description": "The refresh token from the last token response. Bound to the client that received it and to one identity space: presented by another client, or at the other MCP server, it is `invalid_grant` and stays unconsumed."
14937
+ },
14938
+ "client_id": {
14939
+ "type": "string",
14940
+ "minLength": 1,
14941
+ "maxLength": 200,
14942
+ "description": "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
14943
+ },
14944
+ "resource": {
14945
+ "description": "The resource the new token is for, per RFC 8707. Optional; when named it must be the audience the session was issued for, or the answer is `invalid_target` and the refresh token is left untouched. The successor carries the same audience either way.",
14946
+ "type": "string",
14947
+ "format": "uri"
14948
+ }
14949
+ },
14950
+ "required": [
14951
+ "grant_type",
14952
+ "refresh_token",
14953
+ "client_id"
14954
+ ],
14955
+ "description": "RFC 6749 §6's refresh, as either MCP authorization server reads it. Every use rotates: the answer carries a new refresh token and the presented one is dead."
14617
14956
  }
14618
- },
14619
- "required": [
14620
- "grant_type",
14621
- "code",
14622
- "redirect_uri",
14623
- "client_id",
14624
- "code_verifier"
14625
14957
  ],
14626
- "description": "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too."
14958
+ "description": "What an MCP token endpoint accepts: the authorization-code exchange, or a refresh. Any other `grant_type` is `unsupported_grant_type`, refused before a lookup happens."
14627
14959
  },
14628
14960
  "oauth-token-response": {
14629
14961
  "type": "object",
@@ -14645,7 +14977,7 @@
14645
14977
  "description": "How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds."
14646
14978
  },
14647
14979
  "refresh_token": {
14648
- "description": "The refresh token, when one was issued. It rotates on every use.",
14980
+ "description": "The refresh token. Both MCP token endpoints issue one on every exchange and every refresh; it rotates on every use, lives ninety days from its last use, and dies with the account's sessions — a block, a password change, a withdrawn consent. The console's own OAuth portal issues none.",
14649
14981
  "type": "string",
14650
14982
  "minLength": 1
14651
14983
  },