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

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,44 @@
1
1
  # @aventara/mcp
2
2
 
3
+ ## 1.0.0-rc.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [b38034d]
8
+ - Updated dependencies [b38034d]
9
+ - Updated dependencies [b38034d]
10
+ - Updated dependencies [b38034d]
11
+ - Updated dependencies [b38034d]
12
+ - Updated dependencies [b38034d]
13
+ - @aventara/core@1.0.0-rc.3
14
+
15
+ ## 1.0.0-rc.2
16
+
17
+ ### Patch Changes
18
+
19
+ - 9366e16: MCP endpoint polish:
20
+
21
+ - A request with no bearer credentials (no `Authorization` header, or another scheme such as `Basic`) is answered
22
+ `401` with `WWW-Authenticate: Bearer resource_metadata="…"` and no error code, as RFC 6750 §3.1 asks; its body no
23
+ longer says "Missing Authorization header". A bad bearer token keeps `error="invalid_token"`.
24
+ - A `mcp.authorization.verify` that throws is still answered `401`, and is now reported to the `diagnostics` sink as
25
+ an `AvMcpDiagnostic` holding the error.
26
+ - With `mcp` configured and `@aventara/mcp` missing, the startup error names the command that installs it at the
27
+ installed `@aventara/core`'s exact version: `npm install --save-exact @aventara/mcp@<version>`.
28
+
29
+ - A tool call whose `resource` or `variant` holds a lone surrogate (JSON can carry one, a URL cannot) is answered as
30
+ 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
31
+ the tool call, so the agent gets `A2001` (`A2002` for the variant). In rc.1 the name could not be encoded, and the
32
+ call answered the generic `A3000` and reported a diagnostic of kind `mcp`. A `transaction` step that has no canonical
33
+ form (a lone surrogate in a name or a value) is sent with a `null` fingerprint, as core's plan builders and the
34
+ generated client send it, so it is judged as over HTTP: an unknown name is `A2001`/`A2002`, a value `A2004` `V1001`
35
+ at the step's `args`. It answered `V1000` for a missing `fingerprint`, a member the tool does not take.
36
+ - Updated dependencies [9366e16]
37
+ - Updated dependencies [9366e16]
38
+ - Updated dependencies [9366e16]
39
+ - Updated dependencies [9366e16]
40
+ - @aventara/core@1.0.0-rc.2
41
+
3
42
  ## 1.0.0-rc.1
4
43
 
5
44
  ### 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`).
@@ -109,9 +109,12 @@ mcp: {
109
109
 
110
110
  - `GET /.well-known/oauth-protected-resource/mcp` answers the protected-resource metadata (RFC 9728): the endpoint's
111
111
  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
112
+ - A request without `Authorization: Bearer <token>` is `401` with `WWW-Authenticate: Bearer resource_metadata="…"`
113
+ naming that metadata, which is how an MCP client discovers where to sign in. As RFC 6750 asks, it carries no error
114
+ code: the request had no token to be wrong.
115
+ - Each token goes to `verify`. `null` (or `undefined`, or a throw) is `401` with `error="invalid_token"`. A `verify`
116
+ that throws is also reported to your `diagnostics` sink (an `AvMcpDiagnostic` holding the error), so an outage of
117
+ your token check shows in your logs rather than as a run of rejected agents. Anything else lets the request in, and
115
118
  what `verify` returned is the request's `ctx.$state.mcp.claims`, beside what the host's `state` option seeds. Declare
116
119
  its type with the rest of your state:
117
120
 
@@ -24,10 +24,11 @@ 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;
@@ -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 } };
@@ -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
  /**
@@ -105,7 +105,7 @@ async function answerEndpoint(bound, request, state) {
105
105
  const { authorization } = bound.endpoint;
106
106
  const access = authorization === undefined
107
107
  ? loopbackAccess(request.remoteAddress, header)
108
- : await authorizedAccess(header("authorization"), url.origin, bound.endpoint.path, authorization);
108
+ : await authorizedAccess(header("authorization"), url.origin, bound.endpoint.path, authorization, bound.reportFailure);
109
109
  if (!access.ok) {
110
110
  return access.response;
111
111
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/mcp",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.3",
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",
@@ -22,7 +22,7 @@
22
22
  "CHANGELOG.md"
23
23
  ],
24
24
  "dependencies": {
25
- "@aventara/core": "1.0.0-rc.1",
25
+ "@aventara/core": "1.0.0-rc.3",
26
26
  "@modelcontextprotocol/sdk": "1.30.1",
27
27
  "zod": "^4.6.5"
28
28
  },
@@ -30,7 +30,7 @@
30
30
  "access": "public"
31
31
  },
32
32
  "devDependencies": {
33
- "@aventara/testing": "1.0.0-rc.1"
33
+ "@aventara/testing": "1.0.0-rc.3"
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",