@aventara/mcp 1.0.0-rc.1 → 1.0.0-rc.11

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
@@ -1,5 +1,179 @@
1
1
  # @aventara/mcp
2
2
 
3
+ ## 1.0.0-rc.11
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [6197c08]
8
+ - Updated dependencies [e0e89c1]
9
+ - Updated dependencies [186bc29]
10
+ - Updated dependencies [33b5f83]
11
+ - Updated dependencies [6d8b589]
12
+ - Updated dependencies [11ece49]
13
+ - Updated dependencies [92fd093]
14
+ - Updated dependencies [e5e98e6]
15
+ - Updated dependencies [a05d970]
16
+ - @aventara/core@1.0.0-rc.11
17
+
18
+ ## 1.0.0-rc.10
19
+
20
+ ### Patch Changes
21
+
22
+ - 04bb39a: `engines.node` is now `^22.23.4 || >=24.22.1`, the Node security releases of 2026-10-20 (CVE-2026-105481,
23
+ CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074,
24
+ CVE-2026-111497). The bins refuse an older Node in one sentence naming that range.
25
+ - d7e7369: - The tool `me`, offered when the framework's `access` provider states its `client`: it takes `{}` and answers the
26
+ identity Aventara Access resolves from the request's credentials (`{ sub, claims }`, or `null`) as the protocol
27
+ envelope's data, projected onto the `access` section's `me` schema (an identity it does not describe answers
28
+ `A3000`).
29
+ - An endpoint without `authorization` now also requires a loopback `Host` header (`localhost`, `127.x.x.x` or
30
+ `[::1]`, any port); any other `Host`, or none, is `403`. This closes DNS rebinding (the class of CVE-2025-66414): a
31
+ page whose domain is rebound to `127.0.0.1` still names its own domain as `Host`.
32
+ An HTTP/2 caller (`:authority`, no `Host`) is refused by the open endpoint.
33
+ - 3fb3333: `@aventara/mcp` pins `@modelcontextprotocol/sdk` 1.32.2 (was 1.30.1) and `zod` `^4.6.7` (was `^4.6.5`); `@aventara/core`
34
+ also takes `zod` `^4.6.7`. This clears the advisories against those versions:
35
+
36
+ | Advisory | Severity | Package | What it was |
37
+ | ------------------------------------- | -------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
38
+ | 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 |
39
+ | 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 |
40
+ | 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 |
41
+ | GHSA-mrhx-c7xc-m3w5 | high | zod ≤ 4.6.5 | Validating an oversized unconstrained array could exhaust memory |
42
+ | GHSA-rvq5-wwqv-78pq (CVE-2026-111771) | moderate | sdk 1.6.0 – 1.31.x | Server bearer authentication accepted tokens issued for another service |
43
+ | 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 |
44
+
45
+ Aventara's MCP server was already outside most of these: it uses the low-level `Server` (no `taskStore`, no
46
+ `requireBearerAuth`, no SDK OAuth client), and its transport's `maxRequestBodySize` is the agent layer's
47
+ `limits.maxRequestBytes`. No API of `@aventara/mcp` changes.
48
+
49
+ - 88263a1: Documentation: `$access.can([])` answers `[]`, not a refusal; the MCP README states the query and fragment rule for
50
+ every authorization server.
51
+ - Updated dependencies [587dd01]
52
+ - Updated dependencies [d7e7369]
53
+ - Updated dependencies [7ee547b]
54
+ - Updated dependencies [f52c499]
55
+ - Updated dependencies [2907484]
56
+ - Updated dependencies [a6bbdf0]
57
+ - Updated dependencies [3e26549]
58
+ - Updated dependencies [a6bbdf0]
59
+ - Updated dependencies [6f3f6f6]
60
+ - Updated dependencies [04bb39a]
61
+ - Updated dependencies [3fb3333]
62
+ - Updated dependencies [827ccb6]
63
+ - Updated dependencies [7f5abc3]
64
+ - Updated dependencies [88263a1]
65
+ - Updated dependencies [10341d5]
66
+ - Updated dependencies [8129844]
67
+ - Updated dependencies [88263a1]
68
+ - Updated dependencies [3020b5c]
69
+ - Updated dependencies [88263a1]
70
+ - Updated dependencies [88263a1]
71
+ - @aventara/core@1.0.0-rc.10
72
+
73
+ ## 1.0.0-rc.9
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [205a379]
78
+ - Updated dependencies [a0bb05e]
79
+ - Updated dependencies [86ab319]
80
+ - Updated dependencies [fdf52e6]
81
+ - @aventara/core@1.0.0-rc.9
82
+
83
+ ## 1.0.0-rc.8
84
+
85
+ 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.
86
+
87
+ ### Patch Changes
88
+
89
+ - Updated dependencies [1c8e4ee]
90
+ - @aventara/core@1.0.0-rc.8
91
+
92
+ ## 1.0.0-rc.7
93
+
94
+ 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.
95
+
96
+ ### Patch Changes
97
+
98
+ - Updated dependencies [f917c1c]
99
+ - @aventara/core@1.0.0-rc.7
100
+
101
+ ## 1.0.0-rc.6
102
+
103
+ 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.
104
+
105
+ ### Patch Changes
106
+
107
+ - Updated dependencies [1cf72e0]
108
+ - @aventara/core@1.0.0-rc.6
109
+
110
+ ## 1.0.0-rc.5
111
+
112
+ ### Patch Changes
113
+
114
+ - Updated dependencies [4795ff1]
115
+ - Updated dependencies [9e178ca]
116
+ - @aventara/core@1.0.0-rc.5
117
+
118
+ ## 1.0.0-rc.4
119
+
120
+ ### Patch Changes
121
+
122
+ - d10639a: Faster reads. The read-only copies stages are told cost about half what they did: a read with a computed field
123
+ copies each row in one pass, a row with several computed fields shares its nested objects between the stages'
124
+ copies, and after hooks share one frozen copy of the result. A row whose computed field returns a value at once no
125
+ longer waits a turn per row. The MCP endpoint builds its JSON Schema validator once instead of on every request.
126
+ What stages are told, the order they run in and what a request answers are unchanged.
127
+ - Updated dependencies [e3d03a7]
128
+ - Updated dependencies [d10639a]
129
+ - Updated dependencies [f67e68c]
130
+ - Updated dependencies [708556c]
131
+ - Updated dependencies [3a0dfca]
132
+ - Updated dependencies [99169d2]
133
+ - Updated dependencies [0fd2608]
134
+ - Updated dependencies [154e440]
135
+ - Updated dependencies [2fcffdb]
136
+ - @aventara/core@1.0.0-rc.4
137
+
138
+ ## 1.0.0-rc.3
139
+
140
+ ### Patch Changes
141
+
142
+ - Updated dependencies [b38034d]
143
+ - Updated dependencies [b38034d]
144
+ - Updated dependencies [b38034d]
145
+ - Updated dependencies [b38034d]
146
+ - Updated dependencies [b38034d]
147
+ - Updated dependencies [b38034d]
148
+ - @aventara/core@1.0.0-rc.3
149
+
150
+ ## 1.0.0-rc.2
151
+
152
+ ### Patch Changes
153
+
154
+ - 9366e16: MCP endpoint polish:
155
+
156
+ - A request with no bearer credentials (no `Authorization` header, or another scheme such as `Basic`) is answered
157
+ `401` with `WWW-Authenticate: Bearer resource_metadata="…"` and no error code, as RFC 6750 §3.1 asks; its body no
158
+ longer says "Missing Authorization header". A bad bearer token keeps `error="invalid_token"`.
159
+ - A `mcp.authorization.verify` that throws is still answered `401`, and is now reported to the `diagnostics` sink as
160
+ an `AvMcpDiagnostic` holding the error.
161
+ - With `mcp` configured and `@aventara/mcp` missing, the startup error names the command that installs it at the
162
+ installed `@aventara/core`'s exact version: `npm install --save-exact @aventara/mcp@<version>`.
163
+
164
+ - A tool call whose `resource` or `variant` holds a lone surrogate (JSON can carry one, a URL cannot) is answered as
165
+ 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
166
+ the tool call, so the agent gets `A2001` (`A2002` for the variant). In rc.1 the name could not be encoded, and the
167
+ call answered the generic `A3000` and reported a diagnostic of kind `mcp`. A `transaction` step that has no canonical
168
+ form (a lone surrogate in a name or a value) is sent with a `null` fingerprint, as core's plan builders and the
169
+ generated client send it, so it is judged as over HTTP: an unknown name is `A2001`/`A2002`, a value `A2004` `V1001`
170
+ at the step's `args`. It answered `V1000` for a missing `fingerprint`, a member the tool does not take.
171
+ - Updated dependencies [9366e16]
172
+ - Updated dependencies [9366e16]
173
+ - Updated dependencies [9366e16]
174
+ - Updated dependencies [9366e16]
175
+ - @aventara/core@1.0.0-rc.2
176
+
3
177
  ## 1.0.0-rc.1
4
178
 
5
179
  ### 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@rc
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 a `WWW-Authenticate` header naming that metadata,
113
- which is how an MCP client discovers where to sign in.
114
- - Each token goes to `verify`. `null` (or `undefined`, or a throw) is `401`. Anything else lets the request in, and
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`), and no `Origin` header but a
133
- loopback one. Every other caller gets `403`. This is for local development and for an agent running beside the
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
- export type EndpointAuthorization = NonNullable<AvMcpEndpoint["authorization"]>;
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. A missing
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, description) {
27
- const metadata = `${origin}${metadataPathOf(endpointPath)}`;
28
- return jsonAnswer(401, { error: "invalid_token", error_description: description }, {
29
- "WWW-Authenticate": `Bearer error="invalid_token", error_description="${description}", resource_metadata="${metadata}"`,
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, "The access token is invalid"),
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 }
@@ -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
  /**
@@ -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
- export type TranslatedCall = {
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(encodeURIComponent).join("/")}`;
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(fingerprint === undefined ? node : { ...node, fingerprint });
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 undefined;
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, and its plan limit. */
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
- export type McpToolResult = {
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.1",
3
+ "version": "1.0.0-rc.11",
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.18.0 || >=24.2.0"
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.1",
26
- "@modelcontextprotocol/sdk": "1.30.1",
27
- "zod": "^4.6.5"
25
+ "@aventara/core": "1.0.0-rc.11",
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.1"
33
+ "@aventara/testing": "1.0.0-rc.11"
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",