@toa.io/extensions.exposition 1.0.0-alpha.286 → 1.0.0-alpha.288
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 +16 -0
- package/components/identity.basic/tsconfig.tsbuildinfo +1 -1
- package/components/identity.clients/tsconfig.tsbuildinfo +1 -1
- package/components/identity.credentials/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/tsconfig.tsbuildinfo +1 -1
- package/components/identity.grants/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/manifest.toa.yaml +2 -1
- package/components/identity.passkeys/source/errors.test.ts +34 -0
- package/components/identity.passkeys/source/webauthn.test.ts +26 -0
- package/components/identity.passkeys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/types/toa.d.ts +1 -1
- package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
- package/documentation/access.md +22 -4
- package/documentation/discovery.md +81 -0
- package/documentation/help.md +109 -0
- package/documentation/introspection.md +34 -9
- package/documentation/mcp.md +13 -16
- package/features/cors.feature +31 -1
- package/features/dev.feature +2 -0
- package/features/discovery.feature +211 -0
- package/features/discovery.ui.feature +197 -0
- package/features/help.feature +466 -0
- package/features/introspection.feature +94 -8
- package/features/map.feature +1 -0
- package/features/mcp.feature +74 -12
- package/features/methods.feature +2 -1
- package/features/oauth.grants.feature +3 -1
- package/features/octets.download.feature +1 -0
- package/features/octets.meta.feature +1 -0
- package/features/site/_app/immutable/asset.js +1 -0
- package/features/site/favicon.ico +0 -0
- package/features/site/index.html +10 -0
- package/features/steps/Parameters.ts +12 -0
- package/package.json +6 -4
- package/readme.md +3 -0
- package/source/Directive.ts +4 -0
- package/source/Discovery/Explorer.ts +34 -0
- package/source/Discovery/Site.test.ts +147 -0
- package/source/Discovery/Site.ts +202 -0
- package/source/Discovery/index.ts +3 -0
- package/source/Discovery/tree.test.ts +175 -0
- package/source/Discovery/tree.ts +77 -0
- package/source/Discovery/trunk.ts +23 -0
- package/source/Endpoint.ts +4 -0
- package/source/Gateway.ts +34 -4
- package/source/HTTP/Context.ts +7 -0
- package/source/Introspection.ts +33 -10
- package/source/MCP/schema.ts +16 -5
- package/source/MCP/tools.ts +14 -32
- package/source/Mapping.ts +5 -0
- package/source/Query.ts +17 -2
- package/source/RTD/Directives.ts +10 -0
- package/source/RTD/Endpoint.ts +8 -1
- package/source/RTD/Node.ts +20 -6
- package/source/RTD/Tree.ts +19 -9
- package/source/RTD/factory.ts +37 -1
- package/source/RTD/segment.ts +21 -0
- package/source/const.ts +8 -0
- package/source/directives/auth/Anonymous.test.ts +31 -5
- package/source/directives/auth/Anonymous.ts +5 -3
- package/source/directives/auth/Anyone.ts +6 -0
- package/source/directives/auth/Assert.ts +5 -0
- package/source/directives/auth/Delegate.ts +5 -2
- package/source/directives/auth/Federation.ts +11 -0
- package/source/directives/auth/Id.ts +15 -0
- package/source/directives/auth/Input.ts +5 -0
- package/source/directives/auth/Role.ts +17 -0
- package/source/directives/cors/CORS.test.ts +75 -0
- package/source/directives/cors/CORS.ts +10 -3
- package/source/directives/help/Family.ts +118 -0
- package/source/directives/help/Help.test.ts +227 -0
- package/source/directives/help/Help.ts +26 -0
- package/source/directives/help/Parameters.ts +47 -0
- package/source/directives/help/described.ts +57 -0
- package/source/directives/help/index.ts +45 -0
- package/source/directives/index.ts +8 -1
- package/source/directives/map/Headers.ts +6 -6
- package/source/directives/mcp/MCP.ts +12 -21
- package/source/directives/mcp/Tool.test.ts +22 -68
- package/source/directives/mcp/Tool.ts +14 -41
- package/transpiled/Directive.d.ts +1 -0
- package/transpiled/Directive.js +3 -0
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Discovery/Explorer.d.ts +11 -0
- package/transpiled/Discovery/Explorer.js +29 -0
- package/transpiled/Discovery/Explorer.js.map +1 -0
- package/transpiled/Discovery/Site.d.ts +43 -0
- package/transpiled/Discovery/Site.js +171 -0
- package/transpiled/Discovery/Site.js.map +1 -0
- package/transpiled/Discovery/index.d.ts +3 -0
- package/transpiled/Discovery/index.js +4 -0
- package/transpiled/Discovery/index.js.map +1 -0
- package/transpiled/Discovery/tree.d.ts +26 -0
- package/transpiled/Discovery/tree.js +50 -0
- package/transpiled/Discovery/tree.js.map +1 -0
- package/transpiled/Discovery/trunk.d.ts +10 -0
- package/transpiled/Discovery/trunk.js +21 -0
- package/transpiled/Discovery/trunk.js.map +1 -0
- package/transpiled/Endpoint.d.ts +2 -1
- package/transpiled/Endpoint.js +3 -0
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Gateway.d.ts +4 -0
- package/transpiled/Gateway.js +29 -4
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +6 -0
- package/transpiled/HTTP/Context.js +6 -0
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/Introspection.d.ts +18 -7
- package/transpiled/Introspection.js +16 -1
- package/transpiled/Introspection.js.map +1 -1
- package/transpiled/MCP/schema.d.ts +2 -2
- package/transpiled/MCP/schema.js +11 -5
- package/transpiled/MCP/schema.js.map +1 -1
- package/transpiled/MCP/tools.d.ts +1 -1
- package/transpiled/MCP/tools.js +12 -28
- package/transpiled/MCP/tools.js.map +1 -1
- package/transpiled/Mapping.d.ts +2 -0
- package/transpiled/Mapping.js +4 -0
- package/transpiled/Mapping.js.map +1 -1
- package/transpiled/Query.d.ts +12 -0
- package/transpiled/Query.js +16 -2
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +8 -0
- package/transpiled/RTD/Endpoint.d.ts +7 -1
- package/transpiled/RTD/Node.d.ts +11 -2
- package/transpiled/RTD/Node.js +12 -2
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/Tree.js +15 -8
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/RTD/factory.js +27 -1
- package/transpiled/RTD/factory.js.map +1 -1
- package/transpiled/RTD/segment.d.ts +7 -0
- package/transpiled/RTD/segment.js +17 -0
- package/transpiled/RTD/segment.js.map +1 -1
- package/transpiled/const.d.ts +7 -0
- package/transpiled/const.js +7 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/directives/auth/Anonymous.d.ts +4 -2
- package/transpiled/directives/auth/Anonymous.js +5 -3
- package/transpiled/directives/auth/Anonymous.js.map +1 -1
- package/transpiled/directives/auth/Anyone.d.ts +3 -0
- package/transpiled/directives/auth/Anyone.js +4 -0
- package/transpiled/directives/auth/Anyone.js.map +1 -1
- package/transpiled/directives/auth/Assert.d.ts +2 -0
- package/transpiled/directives/auth/Assert.js +4 -0
- package/transpiled/directives/auth/Assert.js.map +1 -1
- package/transpiled/directives/auth/Delegate.d.ts +4 -1
- package/transpiled/directives/auth/Delegate.js +5 -2
- package/transpiled/directives/auth/Delegate.js.map +1 -1
- package/transpiled/directives/auth/Federation.d.ts +5 -0
- package/transpiled/directives/auth/Federation.js +8 -0
- package/transpiled/directives/auth/Federation.js.map +1 -1
- package/transpiled/directives/auth/Id.d.ts +9 -0
- package/transpiled/directives/auth/Id.js +12 -0
- package/transpiled/directives/auth/Id.js.map +1 -1
- package/transpiled/directives/auth/Input.d.ts +2 -0
- package/transpiled/directives/auth/Input.js +4 -0
- package/transpiled/directives/auth/Input.js.map +1 -1
- package/transpiled/directives/auth/Role.d.ts +7 -0
- package/transpiled/directives/auth/Role.js +11 -0
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/cors/CORS.js +9 -2
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/help/Family.d.ts +28 -0
- package/transpiled/directives/help/Family.js +79 -0
- package/transpiled/directives/help/Family.js.map +1 -0
- package/transpiled/directives/help/Help.d.ts +16 -0
- package/transpiled/directives/help/Help.js +22 -0
- package/transpiled/directives/help/Help.js.map +1 -0
- package/transpiled/directives/help/Parameters.d.ts +18 -0
- package/transpiled/directives/help/Parameters.js +34 -0
- package/transpiled/directives/help/Parameters.js.map +1 -0
- package/transpiled/directives/help/described.d.ts +16 -0
- package/transpiled/directives/help/described.js +23 -0
- package/transpiled/directives/help/described.js.map +1 -0
- package/transpiled/directives/help/index.d.ts +22 -0
- package/transpiled/directives/help/index.js +34 -0
- package/transpiled/directives/help/index.js.map +1 -0
- package/transpiled/directives/index.d.ts +4 -0
- package/transpiled/directives/index.js +8 -1
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/map/Headers.d.ts +5 -0
- package/transpiled/directives/map/Headers.js +7 -5
- package/transpiled/directives/map/Headers.js.map +1 -1
- package/transpiled/directives/mcp/MCP.d.ts +8 -5
- package/transpiled/directives/mcp/MCP.js +9 -13
- package/transpiled/directives/mcp/MCP.js.map +1 -1
- package/transpiled/directives/mcp/Tool.d.ts +6 -12
- package/transpiled/directives/mcp/Tool.js +10 -23
- package/transpiled/directives/mcp/Tool.js.map +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/env.js +1 -0
- package/ui/dist/_app/immutable/assets/0.BcIGtTfw.css +2 -0
- package/ui/dist/_app/immutable/assets/2.BC0-Jhqu.css +1 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/ui/dist/_app/immutable/chunks/BgTrzN6u.js +1 -0
- package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/ui/dist/_app/immutable/chunks/CWJaG9mK.js +6 -0
- package/ui/dist/_app/immutable/chunks/DjIYL2zC.js +3 -0
- package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/ui/dist/_app/immutable/chunks/yDQhitF9.js +1 -0
- package/ui/dist/_app/immutable/entry/app.WZSMBcGf.js +2 -0
- package/ui/dist/_app/immutable/entry/start._LkoC6Lz.js +1 -0
- package/ui/dist/_app/immutable/nodes/0.Dl2DqINo.js +1 -0
- package/ui/dist/_app/immutable/nodes/1.CSSnGhVD.js +1 -0
- package/ui/dist/_app/immutable/nodes/2.DmolQyNG.js +14 -0
- package/ui/dist/_app/version.json +1 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/favicon-96x96.png +0 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/index.html +66 -0
|
@@ -62,7 +62,12 @@ Feature: Introspection
|
|
|
62
62
|
required:
|
|
63
63
|
- title
|
|
64
64
|
- volume
|
|
65
|
-
output:
|
|
65
|
+
output:
|
|
66
|
+
type: object
|
|
67
|
+
properties:
|
|
68
|
+
title:
|
|
69
|
+
type: string
|
|
70
|
+
maxLength: 64
|
|
66
71
|
errors:
|
|
67
72
|
- NO_WAY
|
|
68
73
|
- WONT_CREATE
|
|
@@ -79,7 +84,8 @@ Feature: Introspection
|
|
|
79
84
|
/:
|
|
80
85
|
io:output: true
|
|
81
86
|
GET:
|
|
82
|
-
|
|
87
|
+
help:method:
|
|
88
|
+
description: Every pot there is, newest first.
|
|
83
89
|
endpoint: enumerate
|
|
84
90
|
POST: create
|
|
85
91
|
"""
|
|
@@ -214,6 +220,85 @@ Feature: Introspection
|
|
|
214
220
|
POST:
|
|
215
221
|
"""
|
|
216
222
|
|
|
223
|
+
Scenario: A credential does not hide what is anonymous
|
|
224
|
+
A credential refuses an `anonymous` route because it would make the reply uncacheable,
|
|
225
|
+
and a description is not that reply — it is what the resource is, and it varies by who
|
|
226
|
+
asked in any case. Without this an identity would be told less than someone presenting
|
|
227
|
+
nothing at all.
|
|
228
|
+
|
|
229
|
+
Given the `pots` is running with the following manifest:
|
|
230
|
+
"""yaml
|
|
231
|
+
exposition:
|
|
232
|
+
/:
|
|
233
|
+
isolated: true
|
|
234
|
+
anonymous: true
|
|
235
|
+
io:output: [id]
|
|
236
|
+
GET: enumerate
|
|
237
|
+
"""
|
|
238
|
+
When the following request is received:
|
|
239
|
+
"""
|
|
240
|
+
OPTIONS /pots/ HTTP/1.1
|
|
241
|
+
host: nex.toa.io
|
|
242
|
+
authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
|
|
243
|
+
accept: application/yaml
|
|
244
|
+
"""
|
|
245
|
+
Then the following reply is sent:
|
|
246
|
+
"""
|
|
247
|
+
200 OK
|
|
248
|
+
Allow: GET
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
Scenario: A method that takes an identity is not described to nobody
|
|
252
|
+
Which identity it is about cannot be told from a description, which has no route
|
|
253
|
+
variable to read. That there must be one can, and a caller with none is refused
|
|
254
|
+
whatever the value would have been — so they are not shown it.
|
|
255
|
+
|
|
256
|
+
Given the `identity.basic` database contains:
|
|
257
|
+
# developer:secret
|
|
258
|
+
| _id | authority | username | password |
|
|
259
|
+
| efe3a65ebbee47ed95a73edd911ea328 | nex | developer | $2b$10$ZRSKkgZoGnrcTNA5w5eCcu3pxDzdTduhteVYXcp56AaNcilNkwJ.O |
|
|
260
|
+
And the `identity.bans` database is empty
|
|
261
|
+
And the `pots` is running with the following manifest:
|
|
262
|
+
"""yaml
|
|
263
|
+
exposition:
|
|
264
|
+
/:id:
|
|
265
|
+
isolated: true
|
|
266
|
+
io:output: [id]
|
|
267
|
+
GET:
|
|
268
|
+
anonymous: true
|
|
269
|
+
endpoint: observe
|
|
270
|
+
PATCH:
|
|
271
|
+
auth:id: id
|
|
272
|
+
endpoint: assign
|
|
273
|
+
"""
|
|
274
|
+
When the following request is received:
|
|
275
|
+
"""
|
|
276
|
+
OPTIONS /pots/:id/ HTTP/1.1
|
|
277
|
+
host: nex.toa.io
|
|
278
|
+
accept: application/yaml
|
|
279
|
+
"""
|
|
280
|
+
Then the following reply is sent:
|
|
281
|
+
"""
|
|
282
|
+
200 OK
|
|
283
|
+
Allow: GET
|
|
284
|
+
"""
|
|
285
|
+
And the reply does not contain:
|
|
286
|
+
"""
|
|
287
|
+
PATCH:
|
|
288
|
+
"""
|
|
289
|
+
When the following request is received:
|
|
290
|
+
"""
|
|
291
|
+
OPTIONS /pots/:id/ HTTP/1.1
|
|
292
|
+
host: nex.toa.io
|
|
293
|
+
authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
|
|
294
|
+
accept: application/yaml
|
|
295
|
+
"""
|
|
296
|
+
Then the following reply is sent:
|
|
297
|
+
"""
|
|
298
|
+
200 OK
|
|
299
|
+
Allow: GET, PATCH
|
|
300
|
+
"""
|
|
301
|
+
|
|
217
302
|
Scenario: A resource the caller cannot reach at all
|
|
218
303
|
Given the `pots` is running with the following manifest:
|
|
219
304
|
"""yaml
|
|
@@ -274,8 +359,9 @@ Feature: Introspection
|
|
|
274
359
|
"""
|
|
275
360
|
|
|
276
361
|
Scenario: A property a header carries
|
|
277
|
-
`map:headers` fills the property from the request, so it is not the body's to send
|
|
278
|
-
|
|
362
|
+
`map:headers` fills the property from the request, so it is not the body's to send.
|
|
363
|
+
Which header it reads is not answered: what an application takes off a request is its
|
|
364
|
+
own, and a caller has nowhere to put one.
|
|
279
365
|
|
|
280
366
|
Given the `echo` is running with the following manifest:
|
|
281
367
|
"""yaml
|
|
@@ -299,16 +385,16 @@ Feature: Introspection
|
|
|
299
385
|
Allow: PATCH
|
|
300
386
|
|
|
301
387
|
PATCH:
|
|
302
|
-
headers:
|
|
303
|
-
a:
|
|
304
|
-
type: string
|
|
305
|
-
header: x-first
|
|
306
388
|
input:
|
|
307
389
|
type: object
|
|
308
390
|
properties:
|
|
309
391
|
b:
|
|
310
392
|
type: string
|
|
311
393
|
"""
|
|
394
|
+
And the reply does not contain:
|
|
395
|
+
"""
|
|
396
|
+
x-first
|
|
397
|
+
"""
|
|
312
398
|
|
|
313
399
|
Scenario: A property a segment carries under another name
|
|
314
400
|
`map:segments` names the property differently from the segment it reads, so the route
|
package/features/map.feature
CHANGED
package/features/mcp.feature
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
@security
|
|
2
2
|
Feature: Model Context Protocol
|
|
3
3
|
|
|
4
|
-
A tool is a procedure a method is published as. A method says so with `mcp:tool`,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
says
|
|
4
|
+
A tool is a procedure a method is published as. A method says so with `mcp:tool`, and a
|
|
5
|
+
default denies: a tree holds everything an application serves and most of it is machinery
|
|
6
|
+
a model has no business reading. What the tool is called and what it is for is
|
|
7
|
+
`help:method`, which is what describes the method everywhere else as well. What a tool
|
|
8
|
+
takes and answers is what the method says of itself, and what a caller may do with it is
|
|
9
|
+
what `auth` says.
|
|
9
10
|
|
|
10
11
|
What a tool is, is stated on the route and nowhere else. The operation states what it is
|
|
11
12
|
too, and that is not this: one operation mounted on two routes is two tools. A `title`
|
|
@@ -30,20 +31,27 @@ Feature: Model Context Protocol
|
|
|
30
31
|
/:
|
|
31
32
|
io:output: [id, title, volume]
|
|
32
33
|
GET:
|
|
33
|
-
mcp:tool:
|
|
34
|
+
mcp:tool: true
|
|
35
|
+
help:method:
|
|
36
|
+
description: All the pots, newest first.
|
|
34
37
|
endpoint: enumerate
|
|
35
38
|
POST:
|
|
36
|
-
mcp:tool:
|
|
39
|
+
mcp:tool: true
|
|
40
|
+
help:method:
|
|
41
|
+
description: Start a new pot brewing.
|
|
37
42
|
endpoint: create
|
|
38
43
|
/:id:
|
|
39
44
|
GET:
|
|
40
|
-
mcp:tool:
|
|
45
|
+
mcp:tool: true
|
|
46
|
+
help:method:
|
|
47
|
+
description: One pot, by its id.
|
|
41
48
|
endpoint: observe
|
|
42
49
|
/large:
|
|
43
50
|
GET:
|
|
44
51
|
query:
|
|
45
52
|
criteria: volume>=100
|
|
46
|
-
mcp:tool:
|
|
53
|
+
mcp:tool: true
|
|
54
|
+
help:method:
|
|
47
55
|
title: Large pots
|
|
48
56
|
description: The pots that hold a hundred or more, newest first.
|
|
49
57
|
endpoint: enumerate
|
|
@@ -53,7 +61,9 @@ Feature: Model Context Protocol
|
|
|
53
61
|
isolated: true
|
|
54
62
|
GET:
|
|
55
63
|
auth:role: admin
|
|
56
|
-
mcp:tool:
|
|
64
|
+
mcp:tool: true
|
|
65
|
+
help:method:
|
|
66
|
+
description: Every pot there is, for whoever may see them all.
|
|
57
67
|
endpoint: enumerate
|
|
58
68
|
"""
|
|
59
69
|
And the `pots` database contains:
|
|
@@ -256,6 +266,54 @@ Feature: Model Context Protocol
|
|
|
256
266
|
code: -32601
|
|
257
267
|
"""
|
|
258
268
|
|
|
269
|
+
Scenario: A tool that takes an identity is not listed to nobody
|
|
270
|
+
`tools/list` answers what this caller may reach, and it is the description that decides
|
|
271
|
+
— the same one `OPTIONS` answers. A model is not shown a tool it would be refused.
|
|
272
|
+
|
|
273
|
+
Given the annotation:
|
|
274
|
+
"""yaml
|
|
275
|
+
mcp:
|
|
276
|
+
name: Pots
|
|
277
|
+
anonymous: true
|
|
278
|
+
/:
|
|
279
|
+
anonymous: true
|
|
280
|
+
/pots/:id:
|
|
281
|
+
isolated: true
|
|
282
|
+
GET:
|
|
283
|
+
anonymous: true
|
|
284
|
+
mcp:tool: true
|
|
285
|
+
help:method: One pot, by its id.
|
|
286
|
+
dev:stub: []
|
|
287
|
+
PATCH:
|
|
288
|
+
auth:id: id
|
|
289
|
+
mcp:tool: true
|
|
290
|
+
help:method: Change your own pot.
|
|
291
|
+
dev:stub: []
|
|
292
|
+
"""
|
|
293
|
+
When the following request is received:
|
|
294
|
+
"""
|
|
295
|
+
POST /.mcp HTTP/1.1
|
|
296
|
+
host: nex.toa.io
|
|
297
|
+
accept: application/yaml
|
|
298
|
+
content-type: application/json
|
|
299
|
+
mcp-protocol-version: 2026-07-28
|
|
300
|
+
mcp-method: tools/list
|
|
301
|
+
|
|
302
|
+
{"jsonrpc": "2.0", "id": 3, "method": "tools/list",
|
|
303
|
+
"params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
|
304
|
+
"io.modelcontextprotocol/clientCapabilities": {}}}}
|
|
305
|
+
"""
|
|
306
|
+
Then the following reply is sent:
|
|
307
|
+
"""
|
|
308
|
+
200 OK
|
|
309
|
+
|
|
310
|
+
- name: pots._id.GET
|
|
311
|
+
"""
|
|
312
|
+
And the reply does not contain:
|
|
313
|
+
"""
|
|
314
|
+
pots._id.PATCH
|
|
315
|
+
"""
|
|
316
|
+
|
|
259
317
|
Scenario: A credential does not hide an anonymous tool
|
|
260
318
|
A client here always presents one, and what refuses a credentialed request at an
|
|
261
319
|
`anonymous` route is that the reply would not be cacheable — which a tool's is not.
|
|
@@ -563,7 +621,9 @@ Feature: Model Context Protocol
|
|
|
563
621
|
/:
|
|
564
622
|
io:output: true
|
|
565
623
|
GET:
|
|
566
|
-
mcp:tool:
|
|
624
|
+
mcp:tool: true
|
|
625
|
+
help:method:
|
|
626
|
+
description: Refuses, with what it was given.
|
|
567
627
|
endpoint: error
|
|
568
628
|
"""
|
|
569
629
|
When the following request is received:
|
|
@@ -605,7 +665,9 @@ Feature: Model Context Protocol
|
|
|
605
665
|
/:
|
|
606
666
|
io:output: true
|
|
607
667
|
GET:
|
|
608
|
-
mcp:tool:
|
|
668
|
+
mcp:tool: true
|
|
669
|
+
help:method:
|
|
670
|
+
description: Answers with what it was given.
|
|
609
671
|
endpoint: echo
|
|
610
672
|
"""
|
|
611
673
|
When the following request is received:
|
package/features/methods.feature
CHANGED
|
@@ -37,11 +37,12 @@ Feature: Supported methods
|
|
|
37
37
|
When the following request is received:
|
|
38
38
|
"""
|
|
39
39
|
OPTIONS / HTTP/1.1
|
|
40
|
+
access-control-request-method: GET
|
|
40
41
|
host: nex.toa.io
|
|
41
42
|
origin: https://hello.world
|
|
42
43
|
"""
|
|
43
44
|
Then the following reply is sent:
|
|
44
45
|
"""
|
|
45
46
|
204 No Content
|
|
46
|
-
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
|
|
47
|
+
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK, OPTIONS
|
|
47
48
|
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const hashed = true
|
|
Binary file
|
|
@@ -40,6 +40,18 @@ process.env.TOA_TELEMETRY_TRACES ??= JSON.stringify({
|
|
|
40
40
|
}
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
/*
|
|
44
|
+
* The page the gateway serves at `/.discovery/`, pointed at a fixture: the scenarios are
|
|
45
|
+
* about the server, not the page, and none of them should need a UI build. Set here rather
|
|
46
|
+
* than in a step because the gateway is not rebuilt between scenarios that share its
|
|
47
|
+
* signature, so one flipping this would silently read whatever the last one left.
|
|
48
|
+
*/
|
|
49
|
+
process.env.__TESTING_EXPOSITION_DISCOVERY_ROOT = join(
|
|
50
|
+
import.meta.dirname,
|
|
51
|
+
'..',
|
|
52
|
+
'site'
|
|
53
|
+
)
|
|
54
|
+
|
|
43
55
|
const environment = process.env.CLOUDINARY_ENVIRONMENT
|
|
44
56
|
|
|
45
57
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.288",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Exposition",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@node-rs/bcrypt": "1.10.8",
|
|
22
22
|
"@simplewebauthn/server": "14.0.0",
|
|
23
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
23
|
+
"@toa.io/core": "1.0.0-alpha.288",
|
|
24
24
|
"@toa.io/generic": "1.0.0-alpha.286",
|
|
25
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
25
|
+
"@toa.io/schemas": "1.0.0-alpha.287",
|
|
26
26
|
"content-type": "3.0.0",
|
|
27
27
|
"jose": "6.2.10",
|
|
28
28
|
"js-yaml": "5.4.1",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"transpile:otp": "tsc -p ./components/identity.otp",
|
|
49
49
|
"transpile:clients": "tsc -p ./components/identity.clients",
|
|
50
50
|
"transpile:grants": "tsc -p ./components/identity.grants",
|
|
51
|
+
"build:ui": "npm --prefix ui ci && npm --prefix ui run build",
|
|
52
|
+
"prepack": "npm run build:ui",
|
|
51
53
|
"features": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js",
|
|
52
54
|
"features:h2c": "TOA_EXPOSITION_PROTOCOL=h2c TOA_AGENT_PROTOCOL=h2c TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js",
|
|
53
55
|
"features:security": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags @security",
|
|
@@ -58,5 +60,5 @@
|
|
|
58
60
|
"@types/cors": "2.8.19",
|
|
59
61
|
"@types/negotiator": "0.6.5"
|
|
60
62
|
},
|
|
61
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "d3d696f0a4e287c3c297ac81028d600ad7574112"
|
|
62
64
|
}
|
package/readme.md
CHANGED
|
@@ -211,6 +211,9 @@ See also [telemetry ready probe](../telemetry/readme.md#ready-probe) and
|
|
|
211
211
|
- [Resource Tree Definition](documentation/tree.md)
|
|
212
212
|
- [Identity authentication](documentation/identity.md)
|
|
213
213
|
- [Access authorization](documentation/access.md)
|
|
214
|
+
- [Resource introspection](documentation/introspection.md)
|
|
215
|
+
- [Help](documentation/help.md)
|
|
216
|
+
- [Resource discovery](documentation/discovery.md)
|
|
214
217
|
- [OAuth authorization server](documentation/oauth.md)
|
|
215
218
|
- [Consent](documentation/consent.md)
|
|
216
219
|
- [BLOBs](documentation/octets.md)
|
package/source/Directive.ts
CHANGED
|
@@ -206,6 +206,10 @@ export class DirectivesFactory implements RTD.DirectiveFactory {
|
|
|
206
206
|
return directives
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
public inheritable(directive: RTD.syntax.Directive): boolean {
|
|
210
|
+
return this.families[directive.family]?.inherited !== false
|
|
211
|
+
}
|
|
212
|
+
|
|
209
213
|
public dispose(): void {
|
|
210
214
|
for (const directives of this.instances) directives.dispose()
|
|
211
215
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as http from '../HTTP/index.js'
|
|
2
|
+
import { BRANCH_TTL } from '../const.js'
|
|
3
|
+
import { describe } from './tree.js'
|
|
4
|
+
import type { Tree } from '../RTD/index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What an application serves, for every route at once. Each entry is what `OPTIONS` on that
|
|
8
|
+
* path answers, so a key taken from here is a request that can be made.
|
|
9
|
+
*/
|
|
10
|
+
export class Explorer {
|
|
11
|
+
private readonly tree: Tree
|
|
12
|
+
|
|
13
|
+
public constructor(tree: Tree) {
|
|
14
|
+
this.tree = tree
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async process(context: http.Context): Promise<http.OutgoingMessage> {
|
|
18
|
+
if (context.request.method !== 'OPTIONS')
|
|
19
|
+
throw new http.MethodNotAllowed(new Headers({ allow: ALLOW }))
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
body: await describe(this.tree, context),
|
|
23
|
+
headers: new Headers({
|
|
24
|
+
allow: ALLOW,
|
|
25
|
+
// it stands until a branch expires, and it is filtered by who asked
|
|
26
|
+
'cache-control': `private, max-age=${BRANCH_TTL / 1000}`,
|
|
27
|
+
vary: 'authorization'
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The verbs of this path, which are not the verbs of the tree it answers with. */
|
|
34
|
+
const ALLOW = 'GET, HEAD, OPTIONS'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { Readable } from 'node:stream'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import * as fs from 'node:fs'
|
|
6
|
+
import * as os from 'node:os'
|
|
7
|
+
|
|
8
|
+
import { Site } from './Site.js'
|
|
9
|
+
import * as http from '../HTTP/index.js'
|
|
10
|
+
import type { Input, Output } from '../io.js'
|
|
11
|
+
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'discovery-'))
|
|
13
|
+
|
|
14
|
+
fs.mkdirSync(path.join(root, '_app', 'immutable'), { recursive: true })
|
|
15
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'the page\n')
|
|
16
|
+
fs.writeFileSync(path.join(root, 'favicon.ico'), 'icon')
|
|
17
|
+
fs.writeFileSync(path.join(root, '_app', 'immutable', 'asset.js'), 'export {}\n')
|
|
18
|
+
|
|
19
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'discovery-empty-'))
|
|
20
|
+
|
|
21
|
+
const site = new Site(root)
|
|
22
|
+
|
|
23
|
+
const input = (url: string, method = 'GET'): Input =>
|
|
24
|
+
({
|
|
25
|
+
request: { method, headers: {} },
|
|
26
|
+
url: new URL(url, 'http://nex.toa.io')
|
|
27
|
+
}) as unknown as Input
|
|
28
|
+
|
|
29
|
+
const header = (output: Output, name: string): string | null =>
|
|
30
|
+
output?.headers?.get(name) ?? null
|
|
31
|
+
|
|
32
|
+
describe('discovery site', () => {
|
|
33
|
+
it('should leave a path outside its prefix alone', () => {
|
|
34
|
+
assert.equal(site.intercept(input('/')), null)
|
|
35
|
+
assert.equal(site.intercept(input('/pots/')), null)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should not claim a path that merely starts like its own', () => {
|
|
39
|
+
assert.equal(site.intercept(input('/.discoveryable/')), null)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should send the bare path to the directory it is', () => {
|
|
43
|
+
const output = site.intercept(input('/.discovery'))
|
|
44
|
+
|
|
45
|
+
assert.equal(output?.status, 302)
|
|
46
|
+
assert.equal(header(output, 'location'), '/.discovery/')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should keep the query when it redirects', () => {
|
|
50
|
+
const output = site.intercept(input('/.discovery?at=/pots'))
|
|
51
|
+
|
|
52
|
+
assert.equal(header(output, 'location'), '/.discovery/?at=/pots')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should serve the page', () => {
|
|
56
|
+
const output = site.intercept(input('/.discovery/'))
|
|
57
|
+
|
|
58
|
+
assert.equal(output?.status, 200)
|
|
59
|
+
assert.equal(header(output, 'content-type'), 'text/html; charset=utf-8')
|
|
60
|
+
assert.equal(header(output, 'cache-control'), 'no-cache')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should always carry a content-type and a stream', () => {
|
|
64
|
+
// a stream without one is framed as `multipart/*`, and the page arrives as an envelope
|
|
65
|
+
for (const url of ['/.discovery/', '/.discovery/_app/immutable/asset.js']) {
|
|
66
|
+
const output = site.intercept(input(url))
|
|
67
|
+
|
|
68
|
+
assert.ok(output?.headers?.has('content-type'), url)
|
|
69
|
+
assert.ok(output?.body instanceof Readable, url)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should hold a hashed asset forever and an icon for a day', () => {
|
|
74
|
+
assert.equal(
|
|
75
|
+
header(site.intercept(input('/.discovery/_app/immutable/asset.js')), 'cache-control'),
|
|
76
|
+
'public, max-age=31536000, immutable'
|
|
77
|
+
)
|
|
78
|
+
assert.equal(
|
|
79
|
+
header(site.intercept(input('/.discovery/favicon.ico')), 'cache-control'),
|
|
80
|
+
'public, max-age=86400'
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should fall back to the page for anything that could be a route', () => {
|
|
85
|
+
const output = site.intercept(input('/.discovery/pots/:id'))
|
|
86
|
+
|
|
87
|
+
assert.equal(header(output, 'content-type'), 'text/html; charset=utf-8')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should refuse an asset that is not there', () => {
|
|
91
|
+
// a missing asset is missing, however much a route can look like one
|
|
92
|
+
assert.throws(
|
|
93
|
+
() => site.intercept(input('/.discovery/_app/immutable/gone.js')),
|
|
94
|
+
http.NotFound
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should refuse a path that climbs out of the root', () => {
|
|
99
|
+
/*
|
|
100
|
+
* An encoded separator is what reaches here: `..` and `%2e%2e` are dot segments and the
|
|
101
|
+
* URL parser has already collapsed them, but `%2f` keeps a segment whole through it and
|
|
102
|
+
* decodes to one afterwards.
|
|
103
|
+
*/
|
|
104
|
+
for (const url of [
|
|
105
|
+
'/.discovery/%2f%2e%2e%2f%2e%2e%2fpackage.json',
|
|
106
|
+
'/.discovery/a%2f..%2f..%2fpackage.json',
|
|
107
|
+
'/.discovery/%2e%2e%2fpackage.json'
|
|
108
|
+
])
|
|
109
|
+
assert.throws(() => site.intercept(input(url)), http.NotFound, url)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should never see a dot segment, which the URL parser collapses first', () => {
|
|
113
|
+
// `/.discovery/%2e%2e/x` is `/x`, which is not this prefix and not ours to answer
|
|
114
|
+
assert.equal(site.intercept(input('/.discovery/%2e%2e/%2e%2e/package.json')), null)
|
|
115
|
+
assert.equal(site.intercept(input('/.discovery/../../package.json')), null)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should report the length a GET would have returned, and open no stream', () => {
|
|
119
|
+
const output = site.intercept(input('/.discovery/', 'HEAD'))
|
|
120
|
+
|
|
121
|
+
assert.equal(output?.status, 200)
|
|
122
|
+
assert.equal(header(output, 'content-length'), '9')
|
|
123
|
+
assert.equal(output?.body, undefined)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should leave OPTIONS on the prefix to the tree', () => {
|
|
127
|
+
// it needs the identity, which is resolved after this stage
|
|
128
|
+
assert.equal(site.intercept(input('/.discovery', 'OPTIONS')), null)
|
|
129
|
+
assert.equal(site.intercept(input('/.discovery/', 'OPTIONS')), null)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should refuse every other method', () => {
|
|
133
|
+
assert.throws(() => site.intercept(input('/.discovery/', 'POST')), http.MethodNotAllowed)
|
|
134
|
+
assert.throws(
|
|
135
|
+
() => site.intercept(input('/.discovery/favicon.ico', 'OPTIONS')),
|
|
136
|
+
http.MethodNotAllowed
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should say so where the page is not built', () => {
|
|
141
|
+
const output = new Site(empty).intercept(input('/.discovery/'))
|
|
142
|
+
|
|
143
|
+
assert.equal(output?.status, 503)
|
|
144
|
+
assert.equal(header(output, 'content-type'), 'text/plain; charset=utf-8')
|
|
145
|
+
assert.ok(output?.body instanceof Readable)
|
|
146
|
+
})
|
|
147
|
+
})
|