@aventara/mcp 1.0.0-rc.1 → 1.0.0-rc.2
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 +27 -0
- package/README.md +7 -4
- package/dist/endpoint-access.policy.d.ts +4 -3
- package/dist/endpoint-access.policy.js +17 -11
- package/dist/mcp.binder.d.ts +2 -1
- package/dist/mcp.binder.js +1 -1
- package/dist/tool-call.translator.js +8 -5
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @aventara/mcp
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9366e16: MCP endpoint polish:
|
|
8
|
+
|
|
9
|
+
- A request with no bearer credentials (no `Authorization` header, or another scheme such as `Basic`) is answered
|
|
10
|
+
`401` with `WWW-Authenticate: Bearer resource_metadata="…"` and no error code, as RFC 6750 §3.1 asks; its body no
|
|
11
|
+
longer says "Missing Authorization header". A bad bearer token keeps `error="invalid_token"`.
|
|
12
|
+
- A `mcp.authorization.verify` that throws is still answered `401`, and is now reported to the `diagnostics` sink as
|
|
13
|
+
an `AvMcpDiagnostic` holding the error.
|
|
14
|
+
- With `mcp` configured and `@aventara/mcp` missing, the startup error names the command that installs it at the
|
|
15
|
+
installed `@aventara/core`'s exact version: `npm install --save-exact @aventara/mcp@<version>`.
|
|
16
|
+
|
|
17
|
+
- A tool call whose `resource` or `variant` holds a lone surrogate (JSON can carry one, a URL cannot) is answered as
|
|
18
|
+
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
|
|
19
|
+
the tool call, so the agent gets `A2001` (`A2002` for the variant). In rc.1 the name could not be encoded, and the
|
|
20
|
+
call answered the generic `A3000` and reported a diagnostic of kind `mcp`. A `transaction` step that has no canonical
|
|
21
|
+
form (a lone surrogate in a name or a value) is sent with a `null` fingerprint, as core's plan builders and the
|
|
22
|
+
generated client send it, so it is judged as over HTTP: an unknown name is `A2001`/`A2002`, a value `A2004` `V1001`
|
|
23
|
+
at the step's `args`. It answered `V1000` for a missing `fingerprint`, a member the tool does not take.
|
|
24
|
+
- Updated dependencies [9366e16]
|
|
25
|
+
- Updated dependencies [9366e16]
|
|
26
|
+
- Updated dependencies [9366e16]
|
|
27
|
+
- Updated dependencies [9366e16]
|
|
28
|
+
- @aventara/core@1.0.0-rc.2
|
|
29
|
+
|
|
3
30
|
## 1.0.0-rc.1
|
|
4
31
|
|
|
5
32
|
### 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`).
|
|
@@ -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
|
|
113
|
-
which is how an MCP client discovers where to sign in.
|
|
114
|
-
|
|
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.
|
|
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,
|
|
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 } };
|
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
|
@@ -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(
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/mcp",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
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.
|
|
25
|
+
"@aventara/core": "1.0.0-rc.2",
|
|
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.
|
|
33
|
+
"@aventara/testing": "1.0.0-rc.2"
|
|
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",
|