@aventara/mcp 1.0.0-rc.1 → 1.0.0-rc.10
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 +159 -0
- package/README.md +29 -7
- package/dist/endpoint-access.policy.d.ts +6 -4
- package/dist/endpoint-access.policy.js +32 -11
- package/dist/mcp.binder.d.ts +2 -1
- package/dist/mcp.binder.js +42 -3
- package/dist/tool-call.translator.d.ts +2 -1
- package/dist/tool-call.translator.js +8 -5
- package/dist/tool-catalog.compiler.d.ts +8 -3
- package/dist/tool-catalog.compiler.js +16 -0
- package/dist/tool-result.encoder.d.ts +2 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,164 @@
|
|
|
1
1
|
# @aventara/mcp
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 04bb39a: `engines.node` is now `^22.23.4 || >=24.22.1`, the Node security releases of 2026-10-20 (CVE-2026-105481,
|
|
8
|
+
CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074,
|
|
9
|
+
CVE-2026-111497). The bins refuse an older Node in one sentence naming that range.
|
|
10
|
+
- d7e7369: - The tool `me`, offered when the framework's `access` provider states its `client`: it takes `{}` and answers the
|
|
11
|
+
identity Aventara Access resolves from the request's credentials (`{ sub, claims }`, or `null`) as the protocol
|
|
12
|
+
envelope's data, projected onto the `access` section's `me` schema (an identity it does not describe answers
|
|
13
|
+
`A3000`).
|
|
14
|
+
- An endpoint without `authorization` now also requires a loopback `Host` header (`localhost`, `127.x.x.x` or
|
|
15
|
+
`[::1]`, any port); any other `Host`, or none, is `403`. This closes DNS rebinding (the class of CVE-2025-66414): a
|
|
16
|
+
page whose domain is rebound to `127.0.0.1` still names its own domain as `Host`.
|
|
17
|
+
An HTTP/2 caller (`:authority`, no `Host`) is refused by the open endpoint.
|
|
18
|
+
- 3fb3333: `@aventara/mcp` pins `@modelcontextprotocol/sdk` 1.32.2 (was 1.30.1) and `zod` `^4.6.7` (was `^4.6.5`); `@aventara/core`
|
|
19
|
+
also takes `zod` `^4.6.7`. This clears the advisories against those versions:
|
|
20
|
+
|
|
21
|
+
| Advisory | Severity | Package | What it was |
|
|
22
|
+
| ------------------------------------- | -------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| GHSA-46jr-4wx8-v453 (CVE-2026-111768) | high | sdk < 1.32.0 | One HTTP request within the default size limit could exhaust server memory during validation |
|
|
24
|
+
| GHSA-22jm-h49p-29qw (CVE-2026-111767) | high | sdk 1.24.0 – 1.31.x | Experimental tasks were not tied to the session that created them |
|
|
25
|
+
| GHSA-6qxp-vccf-f47h (CVE-2026-104850) | high | sdk 1.12.0 – 1.30.x | The OAuth client could send credentials to an authorization server chosen by the MCP server |
|
|
26
|
+
| GHSA-mrhx-c7xc-m3w5 | high | zod ≤ 4.6.5 | Validating an oversized unconstrained array could exhaust memory |
|
|
27
|
+
| GHSA-rvq5-wwqv-78pq (CVE-2026-111771) | moderate | sdk 1.6.0 – 1.31.x | Server bearer authentication accepted tokens issued for another service |
|
|
28
|
+
| GHSA-6prh-2h8m-c8cw (CVE-2026-111769) | moderate | sdk < 1.32.0 | HTTP client transports and OAuth requests followed cross-origin redirects with custom headers and bodies |
|
|
29
|
+
|
|
30
|
+
Aventara's MCP server was already outside most of these: it uses the low-level `Server` (no `taskStore`, no
|
|
31
|
+
`requireBearerAuth`, no SDK OAuth client), and its transport's `maxRequestBodySize` is the agent layer's
|
|
32
|
+
`limits.maxRequestBytes`. No API of `@aventara/mcp` changes.
|
|
33
|
+
|
|
34
|
+
- 88263a1: Documentation: `$access.can([])` answers `[]`, not a refusal; the MCP README states the query and fragment rule for
|
|
35
|
+
every authorization server.
|
|
36
|
+
- Updated dependencies [587dd01]
|
|
37
|
+
- Updated dependencies [d7e7369]
|
|
38
|
+
- Updated dependencies [7ee547b]
|
|
39
|
+
- Updated dependencies [f52c499]
|
|
40
|
+
- Updated dependencies [2907484]
|
|
41
|
+
- Updated dependencies [a6bbdf0]
|
|
42
|
+
- Updated dependencies [3e26549]
|
|
43
|
+
- Updated dependencies [a6bbdf0]
|
|
44
|
+
- Updated dependencies [6f3f6f6]
|
|
45
|
+
- Updated dependencies [04bb39a]
|
|
46
|
+
- Updated dependencies [3fb3333]
|
|
47
|
+
- Updated dependencies [827ccb6]
|
|
48
|
+
- Updated dependencies [7f5abc3]
|
|
49
|
+
- Updated dependencies [88263a1]
|
|
50
|
+
- Updated dependencies [10341d5]
|
|
51
|
+
- Updated dependencies [8129844]
|
|
52
|
+
- Updated dependencies [88263a1]
|
|
53
|
+
- Updated dependencies [3020b5c]
|
|
54
|
+
- Updated dependencies [88263a1]
|
|
55
|
+
- Updated dependencies [88263a1]
|
|
56
|
+
- @aventara/core@1.0.0-rc.10
|
|
57
|
+
|
|
58
|
+
## 1.0.0-rc.9
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- Updated dependencies [205a379]
|
|
63
|
+
- Updated dependencies [a0bb05e]
|
|
64
|
+
- Updated dependencies [86ab319]
|
|
65
|
+
- Updated dependencies [fdf52e6]
|
|
66
|
+
- @aventara/core@1.0.0-rc.9
|
|
67
|
+
|
|
68
|
+
## 1.0.0-rc.8
|
|
69
|
+
|
|
70
|
+
1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- Updated dependencies [1c8e4ee]
|
|
75
|
+
- @aventara/core@1.0.0-rc.8
|
|
76
|
+
|
|
77
|
+
## 1.0.0-rc.7
|
|
78
|
+
|
|
79
|
+
1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
|
|
80
|
+
|
|
81
|
+
### Patch Changes
|
|
82
|
+
|
|
83
|
+
- Updated dependencies [f917c1c]
|
|
84
|
+
- @aventara/core@1.0.0-rc.7
|
|
85
|
+
|
|
86
|
+
## 1.0.0-rc.6
|
|
87
|
+
|
|
88
|
+
1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
|
|
89
|
+
|
|
90
|
+
### Patch Changes
|
|
91
|
+
|
|
92
|
+
- Updated dependencies [1cf72e0]
|
|
93
|
+
- @aventara/core@1.0.0-rc.6
|
|
94
|
+
|
|
95
|
+
## 1.0.0-rc.5
|
|
96
|
+
|
|
97
|
+
### Patch Changes
|
|
98
|
+
|
|
99
|
+
- Updated dependencies [4795ff1]
|
|
100
|
+
- Updated dependencies [9e178ca]
|
|
101
|
+
- @aventara/core@1.0.0-rc.5
|
|
102
|
+
|
|
103
|
+
## 1.0.0-rc.4
|
|
104
|
+
|
|
105
|
+
### Patch Changes
|
|
106
|
+
|
|
107
|
+
- d10639a: Faster reads. The read-only copies stages are told cost about half what they did: a read with a computed field
|
|
108
|
+
copies each row in one pass, a row with several computed fields shares its nested objects between the stages'
|
|
109
|
+
copies, and after hooks share one frozen copy of the result. A row whose computed field returns a value at once no
|
|
110
|
+
longer waits a turn per row. The MCP endpoint builds its JSON Schema validator once instead of on every request.
|
|
111
|
+
What stages are told, the order they run in and what a request answers are unchanged.
|
|
112
|
+
- Updated dependencies [e3d03a7]
|
|
113
|
+
- Updated dependencies [d10639a]
|
|
114
|
+
- Updated dependencies [f67e68c]
|
|
115
|
+
- Updated dependencies [708556c]
|
|
116
|
+
- Updated dependencies [3a0dfca]
|
|
117
|
+
- Updated dependencies [99169d2]
|
|
118
|
+
- Updated dependencies [0fd2608]
|
|
119
|
+
- Updated dependencies [154e440]
|
|
120
|
+
- Updated dependencies [2fcffdb]
|
|
121
|
+
- @aventara/core@1.0.0-rc.4
|
|
122
|
+
|
|
123
|
+
## 1.0.0-rc.3
|
|
124
|
+
|
|
125
|
+
### Patch Changes
|
|
126
|
+
|
|
127
|
+
- Updated dependencies [b38034d]
|
|
128
|
+
- Updated dependencies [b38034d]
|
|
129
|
+
- Updated dependencies [b38034d]
|
|
130
|
+
- Updated dependencies [b38034d]
|
|
131
|
+
- Updated dependencies [b38034d]
|
|
132
|
+
- Updated dependencies [b38034d]
|
|
133
|
+
- @aventara/core@1.0.0-rc.3
|
|
134
|
+
|
|
135
|
+
## 1.0.0-rc.2
|
|
136
|
+
|
|
137
|
+
### Patch Changes
|
|
138
|
+
|
|
139
|
+
- 9366e16: MCP endpoint polish:
|
|
140
|
+
|
|
141
|
+
- A request with no bearer credentials (no `Authorization` header, or another scheme such as `Basic`) is answered
|
|
142
|
+
`401` with `WWW-Authenticate: Bearer resource_metadata="…"` and no error code, as RFC 6750 §3.1 asks; its body no
|
|
143
|
+
longer says "Missing Authorization header". A bad bearer token keeps `error="invalid_token"`.
|
|
144
|
+
- A `mcp.authorization.verify` that throws is still answered `401`, and is now reported to the `diagnostics` sink as
|
|
145
|
+
an `AvMcpDiagnostic` holding the error.
|
|
146
|
+
- With `mcp` configured and `@aventara/mcp` missing, the startup error names the command that installs it at the
|
|
147
|
+
installed `@aventara/core`'s exact version: `npm install --save-exact @aventara/mcp@<version>`.
|
|
148
|
+
|
|
149
|
+
- A tool call whose `resource` or `variant` holds a lone surrogate (JSON can carry one, a URL cannot) is answered as
|
|
150
|
+
the unknown name it is, as an HTTP call answers it: `fetch` spells a lone surrogate in a URL as U+FFFD, and so does
|
|
151
|
+
the tool call, so the agent gets `A2001` (`A2002` for the variant). In rc.1 the name could not be encoded, and the
|
|
152
|
+
call answered the generic `A3000` and reported a diagnostic of kind `mcp`. A `transaction` step that has no canonical
|
|
153
|
+
form (a lone surrogate in a name or a value) is sent with a `null` fingerprint, as core's plan builders and the
|
|
154
|
+
generated client send it, so it is judged as over HTTP: an unknown name is `A2001`/`A2002`, a value `A2004` `V1001`
|
|
155
|
+
at the step's `args`. It answered `V1000` for a missing `fingerprint`, a member the tool does not take.
|
|
156
|
+
- Updated dependencies [9366e16]
|
|
157
|
+
- Updated dependencies [9366e16]
|
|
158
|
+
- Updated dependencies [9366e16]
|
|
159
|
+
- Updated dependencies [9366e16]
|
|
160
|
+
- @aventara/core@1.0.0-rc.2
|
|
161
|
+
|
|
3
162
|
## 1.0.0-rc.1
|
|
4
163
|
|
|
5
164
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ over the Streamable HTTP transport, stateless. You install this package beside `
|
|
|
10
10
|
yourself.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install @aventara/mcp@
|
|
13
|
+
npm install --save-exact @aventara/mcp@<your @aventara/core version>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
It depends on `@aventara/core` and the official MCP TypeScript SDK (`@modelcontextprotocol/sdk`).
|
|
@@ -49,12 +49,16 @@ One tool per operation family the agent layer exposes, and none for a family not
|
|
|
49
49
|
| `delete` | `{ resource, variant, args }` | destructive |
|
|
50
50
|
| `upsert` | `{ resource, variant, args }` | destructive |
|
|
51
51
|
| `transaction` | `{ steps: [{ resource, family, variant, args? }] }` | destructive |
|
|
52
|
+
| `me` | `{}` | read-only |
|
|
52
53
|
|
|
53
54
|
`resource` and `variant` are enums read from the agent layer, so a resource the agent layer hides is not offered (and is
|
|
54
55
|
refused with `A2001` if an agent names it anyway). `args` is the operation's arguments as the protocol defines them
|
|
55
56
|
(`where`, `select`, `data`, …). `transaction` is offered when the agent layer's transactions are interactive. A step can
|
|
56
57
|
use a value an earlier step returned with `{ "$ref": { "operation": 0, "path": ["id"] } }`; the server adds the
|
|
57
|
-
fingerprints the protocol requires.
|
|
58
|
+
fingerprints the protocol requires. `me` is offered when the framework's `access` provider states its `client`: it
|
|
59
|
+
answers the identity Aventara Access resolves from the request's credentials (`{ sub, claims }`, or `null`) as the
|
|
60
|
+
envelope's data, holding only what the `access` section's `me` schema declares (an identity the schema does not
|
|
61
|
+
describe is the server's own failure, `A3000`). There is no sign-in tool.
|
|
58
62
|
|
|
59
63
|
Every call runs as the `agent` origin, through the same decoding and validation as an HTTP call, with the agent
|
|
60
64
|
layer's pipelines. A success answers the protocol's envelope (`{ data, code, cause }`) as structured content and as
|
|
@@ -107,11 +111,26 @@ mcp: {
|
|
|
107
111
|
},
|
|
108
112
|
```
|
|
109
113
|
|
|
114
|
+
Each server is an https URL. One on your own machine may be http instead, so a local setup needs no TLS:
|
|
115
|
+
`http://127.0.0.1:4000` or `http://[::1]:4000`, with any port and path. That is the whole list. Use the address, not
|
|
116
|
+
`localhost`: a name is looked up, and `localhost` can resolve somewhere else (RFC 8252, section 8.3), so
|
|
117
|
+
`http://localhost:4000` is refused with the spelling to use, `http://127.0.0.1:4000`. `0.0.0.0`, `[::]`, other
|
|
118
|
+
`127.x` addresses, IPv4-mapped addresses, names such as `auth.localhost` and URLs with credentials are refused like
|
|
119
|
+
any other http URL. Write the URL the way a browser would show it: `http://127.1:4000` is refused, and the error gives
|
|
120
|
+
the spelling to use. No server URL, https included, may have a query or fragment (an issuer has neither), an empty
|
|
121
|
+
path segment (`//`) or port 0.
|
|
122
|
+
|
|
123
|
+
An http authorization server is for local development only. Tokens and authorization codes cross it unencrypted, and
|
|
124
|
+
only clients on the same machine can reach it. Deploy with an https server.
|
|
125
|
+
|
|
110
126
|
- `GET /.well-known/oauth-protected-resource/mcp` answers the protected-resource metadata (RFC 9728): the endpoint's
|
|
111
127
|
URL as `resource`, your `servers` as `authorization_servers`, and `bearer_methods_supported: ["header"]`.
|
|
112
|
-
- A request without `Authorization: Bearer <token>` is `401` with
|
|
113
|
-
which is how an MCP client discovers where to sign in.
|
|
114
|
-
|
|
128
|
+
- A request without `Authorization: Bearer <token>` is `401` with `WWW-Authenticate: Bearer resource_metadata="…"`
|
|
129
|
+
naming that metadata, which is how an MCP client discovers where to sign in. As RFC 6750 asks, it carries no error
|
|
130
|
+
code: the request had no token to be wrong.
|
|
131
|
+
- Each token goes to `verify`. `null` (or `undefined`, or a throw) is `401` with `error="invalid_token"`. A `verify`
|
|
132
|
+
that throws is also reported to your `diagnostics` sink (an `AvMcpDiagnostic` holding the error), so an outage of
|
|
133
|
+
your token check shows in your logs rather than as a run of rejected agents. Anything else lets the request in, and
|
|
115
134
|
what `verify` returned is the request's `ctx.$state.mcp.claims`, beside what the host's `state` option seeds. Declare
|
|
116
135
|
its type with the rest of your state:
|
|
117
136
|
|
|
@@ -129,8 +148,11 @@ or Fastify's `trustProxy` for `https` and the public host to appear in the metad
|
|
|
129
148
|
## Without authorization
|
|
130
149
|
|
|
131
150
|
An endpoint without `authorization` takes no token, so it answers callers on the same machine only: a loopback peer
|
|
132
|
-
address, no proxy forwarding header (`Forwarded`, `X-Forwarded-For`, `X-Real-IP`),
|
|
133
|
-
|
|
151
|
+
address, no proxy forwarding header (`Forwarded`, `X-Forwarded-For`, `X-Real-IP`), a loopback `Host` (`localhost`,
|
|
152
|
+
`127.x.x.x` or `[::1]`, any port), and no `Origin` header but a loopback one. Every other caller gets `403`. The `Host`
|
|
153
|
+
rule closes DNS rebinding: a page whose domain is rebound to `127.0.0.1` still sends its own domain as `Host`. An HTTP/2
|
|
154
|
+
caller sends `:authority` instead of `Host`, so the open endpoint refuses it too; serve it over HTTP/1.1, or configure
|
|
155
|
+
`authorization`. This is for local development and for an agent running beside the
|
|
134
156
|
application. To serve remote agents, configure `authorization`.
|
|
135
157
|
|
|
136
158
|
## The transport
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AvMcpEndpoint } from "@aventara/core";
|
|
2
2
|
import type { AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
3
|
/** The authorization `mcp.authorization` configures. */
|
|
4
|
-
|
|
4
|
+
type EndpointAuthorization = NonNullable<AvMcpEndpoint["authorization"]>;
|
|
5
5
|
/** What a request to the endpoint is let through with, or the answer that refuses it. */
|
|
6
6
|
export type EndpointAccess = {
|
|
7
7
|
readonly ok: true;
|
|
@@ -24,10 +24,12 @@ export declare function jsonAnswer(status: number, body: unknown, headers?: Read
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function metadataAnswer(method: string, origin: string, endpointPath: string, authorization: EndpointAuthorization): AvProtocolResponse;
|
|
26
26
|
/**
|
|
27
|
-
* A protected endpoint's check: the request's bearer token, verified.
|
|
27
|
+
* A protected endpoint's check: the request's bearer token, verified. No bearer
|
|
28
28
|
* token, one `verify` rejects (`null`, `undefined`) and a `verify` that throws
|
|
29
|
-
* are each `401`; a good one passes with what `verify` resolved it to.
|
|
29
|
+
* are each `401`; a good one passes with what `verify` resolved it to. A throw
|
|
30
|
+
* is handed to `reportFailure` (the diagnostics sink) as well.
|
|
30
31
|
*/
|
|
31
|
-
export declare function authorizedAccess(authorizationHeader: string | undefined, origin: string, endpointPath: string, authorization: EndpointAuthorization): Promise<EndpointAccess>;
|
|
32
|
+
export declare function authorizedAccess(authorizationHeader: string | undefined, origin: string, endpointPath: string, authorization: EndpointAuthorization, reportFailure: (error: unknown) => void): Promise<EndpointAccess>;
|
|
32
33
|
/** An open endpoint's check: the caller is on this machine. */
|
|
33
34
|
export declare function loopbackAccess(remoteAddress: string | undefined, header: (name: string) => string | undefined): EndpointAccess;
|
|
35
|
+
export {};
|
|
@@ -23,31 +23,37 @@ function bearerTokenOf(header) {
|
|
|
23
23
|
const match = /^Bearer[ \t]+([^ \t].*?)[ \t]*$/i.exec(header ?? "");
|
|
24
24
|
return match?.[1];
|
|
25
25
|
}
|
|
26
|
-
function challenge(origin, endpointPath,
|
|
27
|
-
const metadata =
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
function challenge(origin, endpointPath, invalid) {
|
|
27
|
+
const metadata = `resource_metadata="${origin}${metadataPathOf(endpointPath)}"`;
|
|
28
|
+
if (invalid === undefined) {
|
|
29
|
+
return jsonAnswer(401, {
|
|
30
|
+
error: "unauthorized",
|
|
31
|
+
error_description: "This MCP endpoint requires a bearer token in the Authorization header.",
|
|
32
|
+
}, { "WWW-Authenticate": `Bearer ${metadata}` });
|
|
33
|
+
}
|
|
34
|
+
return jsonAnswer(401, { error: "invalid_token", error_description: invalid.description }, {
|
|
35
|
+
"WWW-Authenticate": `Bearer error="invalid_token", error_description="${invalid.description}", ${metadata}`,
|
|
30
36
|
});
|
|
31
37
|
}
|
|
32
|
-
export async function authorizedAccess(authorizationHeader, origin, endpointPath, authorization) {
|
|
38
|
+
export async function authorizedAccess(authorizationHeader, origin, endpointPath, authorization, reportFailure) {
|
|
33
39
|
const token = bearerTokenOf(authorizationHeader);
|
|
34
40
|
if (token === undefined) {
|
|
35
|
-
return {
|
|
36
|
-
ok: false,
|
|
37
|
-
response: challenge(origin, endpointPath, "Missing Authorization header"),
|
|
38
|
-
};
|
|
41
|
+
return { ok: false, response: challenge(origin, endpointPath) };
|
|
39
42
|
}
|
|
40
43
|
let claims;
|
|
41
44
|
try {
|
|
42
45
|
claims = await authorization.verify(token);
|
|
43
46
|
}
|
|
44
|
-
catch {
|
|
47
|
+
catch (error) {
|
|
48
|
+
reportFailure(error);
|
|
45
49
|
claims = null;
|
|
46
50
|
}
|
|
47
51
|
if (claims === null || claims === undefined) {
|
|
48
52
|
return {
|
|
49
53
|
ok: false,
|
|
50
|
-
response: challenge(origin, endpointPath,
|
|
54
|
+
response: challenge(origin, endpointPath, {
|
|
55
|
+
description: "The access token is invalid",
|
|
56
|
+
}),
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
return { ok: true, caller: { claims } };
|
|
@@ -72,10 +78,25 @@ function isLoopbackOrigin(origin) {
|
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
80
|
const LOOPBACK_ONLY = "This MCP endpoint has no authorization, so it answers callers on this machine only. Configure mcp.authorization to serve remote agents.";
|
|
81
|
+
function isLoopbackHost(host) {
|
|
82
|
+
if (host === undefined || host === "" || /[\s/?#@\\,]/.test(host)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const { hostname } = new URL(`http://${host}`);
|
|
87
|
+
return (hostname === "localhost" ||
|
|
88
|
+
hostname === "[::1]" ||
|
|
89
|
+
isLoopbackAddress(hostname));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
75
95
|
export function loopbackAccess(remoteAddress, header) {
|
|
76
96
|
const origin = header("origin");
|
|
77
97
|
const local = isLoopbackAddress(remoteAddress) &&
|
|
78
98
|
FORWARDING_HEADERS.every((name) => header(name) === undefined) &&
|
|
99
|
+
isLoopbackHost(header("host")) &&
|
|
79
100
|
(origin === undefined || isLoopbackOrigin(origin));
|
|
80
101
|
return local
|
|
81
102
|
? { ok: true }
|
package/dist/mcp.binder.d.ts
CHANGED
|
@@ -51,7 +51,8 @@ export type AvMcpBinding = {
|
|
|
51
51
|
* then the MCP message answered as JSON, statelessly. A tool call runs as an
|
|
52
52
|
* agent-origin protocol request. Never rejects: a failure of its own (the MCP
|
|
53
53
|
* SDK's) is answered `500` with no detail and reported to the Framework's
|
|
54
|
-
* diagnostics sink as an `AvMcpDiagnostic
|
|
54
|
+
* diagnostics sink as an `AvMcpDiagnostic`; a `verify` that throws is
|
|
55
|
+
* answered `401` and reported the same way.
|
|
55
56
|
*/
|
|
56
57
|
readonly handleEndpoint: (request: AvMcpRequest, options?: {
|
|
57
58
|
/**
|
package/dist/mcp.binder.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { mcpFailureReporterOf } from "@aventara/core/internal";
|
|
1
|
+
import { accessProviderOf, mcpFailureReporterOf, projectJsonValue, } from "@aventara/core/internal";
|
|
2
2
|
import { AvProtocol, } from "@aventara/core/protocol";
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5
5
|
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
|
|
6
7
|
import { authorizedAccess, jsonAnswer, loopbackAccess, metadataAnswer, metadataPathOf, } from "./endpoint-access.policy.js";
|
|
7
8
|
import { translateOperationCall, translateTransactionCall, } from "./tool-call.translator.js";
|
|
8
|
-
import { CONTRACT_RESOURCE_URI, compileToolCatalog, OPERATION_FAMILIES, TRANSACTION_TOOL, } from "./tool-catalog.compiler.js";
|
|
9
|
+
import { CONTRACT_RESOURCE_URI, compileToolCatalog, ME_TOOL, OPERATION_FAMILIES, TRANSACTION_TOOL, } from "./tool-catalog.compiler.js";
|
|
9
10
|
import { internalFailureResult, refusedCallResult, toolResultOf, } from "./tool-result.encoder.js";
|
|
10
11
|
const SERVER_INSTRUCTIONS = `This server reads and writes an application's data through Aventara. Read the resource ${CONTRACT_RESOURCE_URI} first: it describes every resource, its fields and the operations it offers. Then call a tool with { resource, variant, args }. A failed call answers isError with the outcome code and the issues to fix.`;
|
|
11
12
|
export function bind(framework) {
|
|
@@ -15,6 +16,7 @@ export function bind(framework) {
|
|
|
15
16
|
}
|
|
16
17
|
const agent = framework.contracts.agent;
|
|
17
18
|
const protocol = AvProtocol.bind(framework, { origin: "agent" });
|
|
19
|
+
const me = accessProviderOf(framework)?.client?.verbs.me;
|
|
18
20
|
const bound = {
|
|
19
21
|
endpoint,
|
|
20
22
|
protocol,
|
|
@@ -22,6 +24,7 @@ export function bind(framework) {
|
|
|
22
24
|
tools: compileToolCatalog({
|
|
23
25
|
surface: protocol.surface(),
|
|
24
26
|
maxTransactionOperations: agent.limits.maxTransactionOperations,
|
|
27
|
+
me: me !== undefined,
|
|
25
28
|
}),
|
|
26
29
|
contractDocument: protocol.encodeContract({
|
|
27
30
|
method: "GET",
|
|
@@ -31,6 +34,8 @@ export function bind(framework) {
|
|
|
31
34
|
}).body,
|
|
32
35
|
maxRequestBytes: agent.limits.maxRequestBytes,
|
|
33
36
|
reportFailure: mcpFailureReporterOf(framework),
|
|
37
|
+
schemaValidator: new AjvJsonSchemaValidator(),
|
|
38
|
+
me,
|
|
34
39
|
};
|
|
35
40
|
const routes = Object.freeze([
|
|
36
41
|
Object.freeze({
|
|
@@ -105,7 +110,7 @@ async function answerEndpoint(bound, request, state) {
|
|
|
105
110
|
const { authorization } = bound.endpoint;
|
|
106
111
|
const access = authorization === undefined
|
|
107
112
|
? loopbackAccess(request.remoteAddress, header)
|
|
108
|
-
: await authorizedAccess(header("authorization"), url.origin, bound.endpoint.path, authorization);
|
|
113
|
+
: await authorizedAccess(header("authorization"), url.origin, bound.endpoint.path, authorization, bound.reportFailure);
|
|
109
114
|
if (!access.ok) {
|
|
110
115
|
return access.response;
|
|
111
116
|
}
|
|
@@ -186,6 +191,7 @@ function serverFor(bound, headers, state) {
|
|
|
186
191
|
const server = new Server({ name: "aventara", title: "Aventara", version: bound.identity.hash }, {
|
|
187
192
|
capabilities: { tools: {}, resources: {} },
|
|
188
193
|
instructions: SERVER_INSTRUCTIONS,
|
|
194
|
+
jsonSchemaValidator: bound.schemaValidator,
|
|
189
195
|
});
|
|
190
196
|
const options = state === undefined ? {} : { state };
|
|
191
197
|
server.setRequestHandler(ListToolsRequestSchema, guarded(bound, () => ({ tools: bound.tools })));
|
|
@@ -242,6 +248,9 @@ function guarded(bound, handler) {
|
|
|
242
248
|
};
|
|
243
249
|
}
|
|
244
250
|
async function callTool(bound, name, input, headers, options) {
|
|
251
|
+
if (name === ME_TOOL) {
|
|
252
|
+
return (await answerMe(bound, input, headers));
|
|
253
|
+
}
|
|
245
254
|
if (name === TRANSACTION_TOOL) {
|
|
246
255
|
const translated = await translateTransactionCall(input, headers, bound.identity);
|
|
247
256
|
return (translated.ok
|
|
@@ -254,3 +263,33 @@ async function callTool(bound, name, input, headers, options) {
|
|
|
254
263
|
? toolResultOf(await bound.protocol.handleOperation(translated.request, options))
|
|
255
264
|
: refusedCallResult(translated.message));
|
|
256
265
|
}
|
|
266
|
+
async function answerMe(bound, input, headers) {
|
|
267
|
+
if (input !== undefined &&
|
|
268
|
+
(typeof input !== "object" ||
|
|
269
|
+
input === null ||
|
|
270
|
+
Array.isArray(input) ||
|
|
271
|
+
Object.keys(input).length > 0)) {
|
|
272
|
+
return refusedCallResult("me takes no arguments: call it with {}.");
|
|
273
|
+
}
|
|
274
|
+
const me = bound.me;
|
|
275
|
+
if (me === undefined) {
|
|
276
|
+
throw new Error("The me tool was called on a binding that offers none.");
|
|
277
|
+
}
|
|
278
|
+
const identity = await me.call({
|
|
279
|
+
args: undefined,
|
|
280
|
+
headers: webHeadersOf(headers),
|
|
281
|
+
});
|
|
282
|
+
const data = identity === null || identity === undefined
|
|
283
|
+
? null
|
|
284
|
+
: projectJsonValue(identity, me.result);
|
|
285
|
+
return toolResultOf(jsonAnswer(200, { code: "A1000", data, cause: null }));
|
|
286
|
+
}
|
|
287
|
+
function webHeadersOf(headers) {
|
|
288
|
+
const web = new Headers();
|
|
289
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
290
|
+
for (const item of typeof value === "string" ? [value] : (value ?? [])) {
|
|
291
|
+
web.append(name, item);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return web;
|
|
295
|
+
}
|
|
@@ -6,7 +6,7 @@ export type AgentIdentity = {
|
|
|
6
6
|
readonly hash: string;
|
|
7
7
|
};
|
|
8
8
|
/** A tool call as the protocol request that runs it, or why it cannot be one. */
|
|
9
|
-
|
|
9
|
+
type TranslatedCall = {
|
|
10
10
|
readonly ok: true;
|
|
11
11
|
readonly kind: "operation" | "transaction";
|
|
12
12
|
readonly request: AvProtocolRequest;
|
|
@@ -30,3 +30,4 @@ export declare function translateOperationCall(family: OperationFamily, input: u
|
|
|
30
30
|
* left as written, for the protocol to refuse.
|
|
31
31
|
*/
|
|
32
32
|
export declare function translateTransactionCall(input: unknown, received: Readonly<Record<string, string | readonly string[] | undefined>>, identity: AgentIdentity): Promise<TranslatedCall>;
|
|
33
|
+
export {};
|
|
@@ -32,6 +32,9 @@ function protocolRequest(path, body, headers) {
|
|
|
32
32
|
body: { kind: "raw", content: jsonTextOf(body) },
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
function routeSegmentOf(name) {
|
|
36
|
+
return encodeURIComponent(name.replace(/\p{Surrogate}/gu, "\uFFFD"));
|
|
37
|
+
}
|
|
35
38
|
export function translateOperationCall(family, input, received, identity) {
|
|
36
39
|
if (!isRecord(input)) {
|
|
37
40
|
return {
|
|
@@ -46,7 +49,7 @@ export function translateOperationCall(family, input, received, identity) {
|
|
|
46
49
|
message: `${family}: \`resource\` and \`variant\` must be strings (read ${CONTRACT_RESOURCE_URI}).`,
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
|
-
const path = `/_resources/${[resource, family, variant].map(
|
|
52
|
+
const path = `/_resources/${[resource, family, variant].map(routeSegmentOf).join("/")}`;
|
|
50
53
|
return {
|
|
51
54
|
ok: true,
|
|
52
55
|
kind: "operation",
|
|
@@ -80,8 +83,8 @@ export async function translateTransactionCall(input, received, identity) {
|
|
|
80
83
|
args,
|
|
81
84
|
};
|
|
82
85
|
const fingerprint = await fingerprintOf(node);
|
|
83
|
-
fingerprints.push(fingerprint
|
|
84
|
-
operations.push(
|
|
86
|
+
fingerprints.push(fingerprint);
|
|
87
|
+
operations.push({ ...node, fingerprint });
|
|
85
88
|
}
|
|
86
89
|
return {
|
|
87
90
|
ok: true,
|
|
@@ -94,7 +97,7 @@ async function fingerprintOf(node) {
|
|
|
94
97
|
return await computeOperationFingerprint(node);
|
|
95
98
|
}
|
|
96
99
|
catch {
|
|
97
|
-
return
|
|
100
|
+
return null;
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
function referenced(value, fingerprints) {
|
|
@@ -163,7 +166,7 @@ function withFingerprint(value, fingerprints) {
|
|
|
163
166
|
const fingerprint = typeof operation === "number" && Number.isInteger(operation)
|
|
164
167
|
? fingerprints[operation]
|
|
165
168
|
: undefined;
|
|
166
|
-
return fingerprint === undefined || fingerprint ===
|
|
169
|
+
return fingerprint === undefined || fingerprint === null
|
|
167
170
|
? value
|
|
168
171
|
: { $ref: { ...reference, fingerprint } };
|
|
169
172
|
}
|
|
@@ -5,8 +5,10 @@ export declare const OPERATION_FAMILIES: readonly ["find", "create", "update", "
|
|
|
5
5
|
export type OperationFamily = (typeof OPERATION_FAMILIES)[number];
|
|
6
6
|
/** The name of the tool that runs a plan of operations. */
|
|
7
7
|
export declare const TRANSACTION_TOOL = "transaction";
|
|
8
|
+
/** The name of the tool that answers who the agent acts for. */
|
|
9
|
+
export declare const ME_TOOL = "me";
|
|
8
10
|
/** Every tool name the catalog can offer. */
|
|
9
|
-
export type McpToolName = OperationFamily | typeof TRANSACTION_TOOL;
|
|
11
|
+
export type McpToolName = OperationFamily | typeof TRANSACTION_TOOL | typeof ME_TOOL;
|
|
10
12
|
/** An MCP tool's behaviour hints. */
|
|
11
13
|
export type McpToolAnnotations = {
|
|
12
14
|
readonly readOnlyHint: boolean;
|
|
@@ -31,14 +33,17 @@ export type McpToolDefinition = {
|
|
|
31
33
|
};
|
|
32
34
|
/** The URI of the MCP resource that holds the Agent Contract. */
|
|
33
35
|
export declare const CONTRACT_RESOURCE_URI = "aventara://contract";
|
|
34
|
-
/** What the catalog is compiled from: the agent binding's surface,
|
|
36
|
+
/** What the catalog is compiled from: the agent binding's surface, its plan limit, and whether `me` is offered. */
|
|
35
37
|
export type CatalogSource = {
|
|
36
38
|
readonly surface: ReturnType<AvProtocolBinding["surface"]>;
|
|
37
39
|
readonly maxTransactionOperations: number;
|
|
40
|
+
/** The access provider states its `client`, so `me` has an answer. */
|
|
41
|
+
readonly me?: boolean;
|
|
38
42
|
};
|
|
39
43
|
/**
|
|
40
44
|
* The tools the agent binding's surface supports: one per family it offers, in
|
|
41
45
|
* the protocol's family order, then `transaction` when the surface has the
|
|
42
|
-
* transactions route and at least one operation
|
|
46
|
+
* transactions route and at least one operation, then `me` when the access
|
|
47
|
+
* provider states its `client`.
|
|
43
48
|
*/
|
|
44
49
|
export declare function compileToolCatalog(source: CatalogSource): readonly McpToolDefinition[];
|
|
@@ -6,6 +6,7 @@ export const OPERATION_FAMILIES = [
|
|
|
6
6
|
"upsert",
|
|
7
7
|
];
|
|
8
8
|
export const TRANSACTION_TOOL = "transaction";
|
|
9
|
+
export const ME_TOOL = "me";
|
|
9
10
|
const VARIANT_ORDER = {
|
|
10
11
|
find: ["first", "unique", "many", "count"],
|
|
11
12
|
create: ["one", "many", "count"],
|
|
@@ -59,6 +60,7 @@ const ANNOTATIONS = {
|
|
|
59
60
|
destructiveHint: true,
|
|
60
61
|
openWorldHint: false,
|
|
61
62
|
},
|
|
63
|
+
me: { readOnlyHint: true, openWorldHint: false },
|
|
62
64
|
};
|
|
63
65
|
export const CONTRACT_RESOURCE_URI = "aventara://contract";
|
|
64
66
|
const ENVELOPE_SCHEMA = {
|
|
@@ -193,6 +195,17 @@ function transactionTool(offers, maxTransactionOperations) {
|
|
|
193
195
|
annotations: ANNOTATIONS.transaction,
|
|
194
196
|
};
|
|
195
197
|
}
|
|
198
|
+
const ME_TOOL_DEFINITION = {
|
|
199
|
+
name: ME_TOOL,
|
|
200
|
+
title: "Who is signed in",
|
|
201
|
+
description: [
|
|
202
|
+
"Answers who this agent acts for: the identity Aventara Access resolved from this request's credentials, { sub, claims }, as the data, or null when no one is signed in.",
|
|
203
|
+
"Takes no arguments: call it with {}.",
|
|
204
|
+
].join(" "),
|
|
205
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
206
|
+
outputSchema: ENVELOPE_SCHEMA,
|
|
207
|
+
annotations: ANNOTATIONS.me,
|
|
208
|
+
};
|
|
196
209
|
export function compileToolCatalog(source) {
|
|
197
210
|
const offers = offersOf(source.surface);
|
|
198
211
|
const tools = [];
|
|
@@ -206,5 +219,8 @@ export function compileToolCatalog(source) {
|
|
|
206
219
|
source.surface.some((route) => route.kind === "transactions")) {
|
|
207
220
|
tools.push(transactionTool(offers, source.maxTransactionOperations));
|
|
208
221
|
}
|
|
222
|
+
if (source.me === true) {
|
|
223
|
+
tools.push(ME_TOOL_DEFINITION);
|
|
224
|
+
}
|
|
209
225
|
return Object.freeze(tools);
|
|
210
226
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AvOperationFailureResponse } from "@aventara/core";
|
|
2
2
|
import type { AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
3
|
/** A tool call's result, as `tools/call` answers it. */
|
|
4
|
-
|
|
4
|
+
type McpToolResult = {
|
|
5
5
|
readonly content: readonly [{
|
|
6
6
|
readonly type: "text";
|
|
7
7
|
readonly text: string;
|
|
@@ -25,3 +25,4 @@ export declare function refusedCallResult(message: string): McpToolResult;
|
|
|
25
25
|
* protocol's own `A3000`. Nothing of the thrown error is in it.
|
|
26
26
|
*/
|
|
27
27
|
export declare function internalFailureResult(envelope: AvOperationFailureResponse): McpToolResult;
|
|
28
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/mcp",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.10",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "The MCP server for an Aventara application: its agent layer as Model Context Protocol tools.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": "^22.
|
|
8
|
+
"node": "^22.23.4 || >=24.22.1"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
"CHANGELOG.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@aventara/core": "1.0.0-rc.
|
|
26
|
-
"@modelcontextprotocol/sdk": "1.
|
|
27
|
-
"zod": "^4.6.
|
|
25
|
+
"@aventara/core": "1.0.0-rc.10",
|
|
26
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
27
|
+
"zod": "^4.6.7"
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@aventara/testing": "1.0.0-rc.
|
|
33
|
+
"@aventara/testing": "1.0.0-rc.10"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
|