@webpieces/mcp-server 0.4.863 → 0.4.864
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/README.md +27 -24
- package/package.json +6 -6
- package/src/McpApiBinding.d.ts +3 -2
- package/src/McpApiBinding.js +12 -9
- package/src/McpApiBinding.js.map +1 -1
- package/src/McpApiDispatcher.d.ts +2 -2
- package/src/McpApiDispatcher.js +3 -3
- package/src/McpApiDispatcher.js.map +1 -1
- package/src/McpAuth.d.ts +12 -32
- package/src/McpAuth.js +14 -53
- package/src/McpAuth.js.map +1 -1
- package/src/McpToolPolicy.d.ts +6 -6
- package/src/McpToolPolicy.js +6 -4
- package/src/McpToolPolicy.js.map +1 -1
- package/src/McpToolRegistry.d.ts +7 -6
- package/src/McpToolRegistry.js +16 -11
- package/src/McpToolRegistry.js.map +1 -1
- package/src/WpMcpServer.d.ts +2 -3
- package/src/WpMcpServer.js +3 -16
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpRemoteFixtures.d.ts +2 -1
- package/src/__tests__/McpRemoteFixtures.js +83 -81
- package/src/__tests__/McpRemoteFixtures.js.map +1 -1
- package/src/__tests__/WpMcpServerTestFixtures.js +42 -41
- package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -1
- package/src/index.d.ts +2 -2
- package/src/index.js +1 -3
- package/src/index.js.map +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@ and output JSON Schema — the build extracts them from the contract source and
|
|
|
5
5
|
`mcp-<ContractClass>-tools.json` per contract — invokes the normal Webpieces filter/controller path, and turns exceptions into safe model-visible
|
|
6
6
|
MCP results.
|
|
7
7
|
|
|
8
|
-
`@WpMcpTool`
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
`@WpMcpTool` opts an endpoint into the protocol. Every endpoint declares canonical
|
|
9
|
+
`@WpAuth` credentials and common `@WpAuthorization` policy. The same policy governs tool visibility,
|
|
10
|
+
schema lookup, dispatch and receiving endpoint execution. The MCP adapter never accepts trusted
|
|
11
|
+
context from tool arguments or unverified headers.
|
|
11
12
|
|
|
12
13
|
```ts
|
|
13
14
|
interface FindOrderRequest {
|
|
@@ -24,8 +25,8 @@ interface FindOrderResponse {
|
|
|
24
25
|
@ApiType(SVC_TO_SVC, MCP)
|
|
25
26
|
abstract class OrdersApi {
|
|
26
27
|
/** Find one order owned by the signed-in user. */
|
|
27
|
-
@
|
|
28
|
-
@
|
|
28
|
+
@WpAuth([jwt()])
|
|
29
|
+
@WpAuthorization({ authType: AuthorizationType.ALL_USERS })
|
|
29
30
|
@Endpoint(POST, '/find', READ, RPC)
|
|
30
31
|
@WpMcpTool('orders_find')
|
|
31
32
|
find(request: FindOrderRequest): Promise<FindOrderResponse> {
|
|
@@ -101,13 +102,12 @@ settings can be swapped, and a future setting is an additive setter rather than
|
|
|
101
102
|
change. Expect to supply the type arguments explicitly:
|
|
102
103
|
|
|
103
104
|
```ts
|
|
104
|
-
const config = new WpMcpServerConfig<MyGrant
|
|
105
|
+
const config = new WpMcpServerConfig<MyGrant>()
|
|
105
106
|
.setName('my-server')
|
|
106
107
|
.setVersion('1.0.0')
|
|
107
108
|
.setResource('https://api.example.com/mcp')
|
|
108
109
|
.setAccessTokenAuthority(authority)
|
|
109
|
-
.
|
|
110
|
-
.setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))
|
|
110
|
+
.setAuthorizationService(router.authorizationService())
|
|
111
111
|
.setAuthorizationServers(['https://login.example.com'])
|
|
112
112
|
.setRequiredScopes(['tools'])
|
|
113
113
|
.setMaxAccountValidationAgeSeconds(15 * 60);
|
|
@@ -176,13 +176,13 @@ supplied `_meta.progressToken`) a `reportProgress(...)` callback. Progress remai
|
|
|
176
176
|
one request and causes the official handler to select request-scoped SSE; closing that response
|
|
177
177
|
aborts the same signal. The context key has no HTTP header and is never propagated as caller input.
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
Local bindings require explicit tool exposure and valid common authorization. They pass a token-free
|
|
180
|
+
`InvocationAuthentication` through `ApiFactory.createInvocationApiClient`, preserving the ordinary
|
|
181
|
+
logging, authentication and authorization filters. The proof identifies the actual endpoint and
|
|
182
|
+
original verified ingress scope. No endpoint JWT is minted or parsed and no GUI `JWT_HOOK` is required.
|
|
183
|
+
Remote bindings declare `oidc(...)` or `sharedSecret(...)` network credentials. The generated Node
|
|
184
|
+
client chooses its service mechanism before forwarding verified delegated facts; the MCP bearer never
|
|
185
|
+
becomes a downstream credential.
|
|
186
186
|
|
|
187
187
|
`McpDeployment.singleProcess()` uses an in-memory invalidation bus and makes that limit explicit.
|
|
188
188
|
Distributed deployments must use `McpDeployment.distributed(sharedBus)`. List responses have a bounded
|
|
@@ -194,32 +194,35 @@ The authority verifies issuer, signature/token state, expiry, scopes, the exact
|
|
|
194
194
|
current account state. Neither served era keeps a session here, so the bearer is verified once per POST
|
|
195
195
|
at the `bind(...)` HTTP boundary, before the SDK is involved; tool handlers never re-verify it. A
|
|
196
196
|
rejected token must be thrown as `ApiUnauthorizedError` (answered 401 + `WWW-Authenticate`); any
|
|
197
|
-
other throw from the authority is an implementation failure (500).
|
|
198
|
-
hour and MCP access tokens at 30 days.
|
|
197
|
+
other throw from the authority is an implementation failure (500). MCP access tokens are capped at 30 days.
|
|
199
198
|
|
|
200
199
|
## Principal, policy and request context
|
|
201
200
|
|
|
202
201
|
`VerifiedMcpCredential.caller` is the application's `AuthenticatedCaller` resolved from current account
|
|
203
202
|
facts. `principalRoles` is derived from that caller, a public hard cut from the old listing-role field.
|
|
204
|
-
|
|
203
|
+
The framework publishes its canonical USER_ID and JSON-array USER_ROLES; custom trusted facts come from the verified caller entries. Security metadata
|
|
205
204
|
(issuer, exact resource, scopes, issuance/expiry and account validation time) remains on the credential.
|
|
206
205
|
|
|
207
206
|
The framework publishes the caller's trusted entries and `llm` surface immediately after verification,
|
|
208
207
|
then reconciles inbound trusted headers using the same operation as ordinary HTTP. Contradictory or
|
|
209
|
-
unvouched identity/surface headers are rejected. `
|
|
208
|
+
unvouched identity/surface headers are rejected. The common `AuthorizationService` governs the request's authorized
|
|
210
209
|
tool view before SDK processing; only explicit `ApiForbiddenError` denials hide tools. Policy/datastore
|
|
211
210
|
failures propagate to the safe ingress boundary. Documentation, synchronous SDK schema lookup and call
|
|
212
211
|
name resolution all use that view. The dispatcher independently checks the same policy before input
|
|
213
|
-
validation and
|
|
214
|
-
schema validation
|
|
212
|
+
validation and invocation. A hidden tool is indistinguishable from an unknown name and triggers no
|
|
213
|
+
schema validation or binding invocation.
|
|
215
214
|
|
|
216
215
|
Each HTTP POST has one transport-owned `RequestContext.run`, including malformed-body failures. Local
|
|
217
216
|
invocation opens no detached/nested scope and retains the original transport request, request/action IDs,
|
|
218
217
|
custom context and downstream response-header accumulation. Its immutable `InvocationAuthentication`
|
|
219
218
|
is carried on that invocation's proxy/MethodMeta, never on a mutable ambient credential slot. Normal
|
|
220
|
-
endpoint
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
receiving endpoint authorization runs against the same canonical trusted facts. A captured/restored
|
|
220
|
+
snapshot cannot reuse the local proof. GUI HTTP requests verify their own bearer normally. Remote
|
|
221
|
+
bindings authenticate their own hop before accepting delegated facts; another HTTP process has its
|
|
222
|
+
own scope. `SERVICE_ONLY` requires actual current-hop machine evidence, even when the surface is `llm`.
|
|
223
|
+
|
|
224
|
+
See [the authorization migration and release order](../../../docs/authorization-migration.md).
|
|
225
|
+
|
|
223
226
|
|
|
224
227
|
## Error boundary
|
|
225
228
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.864",
|
|
4
4
|
"description": "Secure MCP tool generation from annotated Webpieces API contracts",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/node": "2.0.0",
|
|
26
26
|
"@modelcontextprotocol/server": "2.0.0",
|
|
27
|
-
"@webpieces/core-context": "0.4.
|
|
28
|
-
"@webpieces/core-util": "0.4.
|
|
29
|
-
"@webpieces/http-routing": "0.4.
|
|
30
|
-
"@webpieces/http-server": "0.4.
|
|
27
|
+
"@webpieces/core-context": "0.4.864",
|
|
28
|
+
"@webpieces/core-util": "0.4.864",
|
|
29
|
+
"@webpieces/http-routing": "0.4.864",
|
|
30
|
+
"@webpieces/http-server": "0.4.864",
|
|
31
31
|
"express": "5.1.0",
|
|
32
32
|
"reflect-metadata": "0.2.2",
|
|
33
33
|
"tslib": "2.8.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@webpieces/http-client-node": "0.4.
|
|
36
|
+
"@webpieces/http-client-node": "0.4.864"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/McpApiBinding.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { DtoValue } from '@webpieces/core-util';
|
|
2
|
-
import { ApiFactory, ClassType, InvocationAuthentication } from '@webpieces/http-routing';
|
|
2
|
+
import { ApiFactory, AuthorizationService, ClassType, InvocationAuthentication } from '@webpieces/http-routing';
|
|
3
3
|
export type McpBindingTopology = 'local' | 'remote';
|
|
4
4
|
/** Explicit contract-to-invoker binding; MCP never discovers implementations from a router. */
|
|
5
5
|
export declare class McpApiBinding<TApi extends object = object> {
|
|
6
6
|
readonly api: ClassType<TApi>;
|
|
7
7
|
readonly topology: McpBindingTopology;
|
|
8
8
|
private readonly provider;
|
|
9
|
+
private readonly receivingPolicy?;
|
|
9
10
|
private constructor();
|
|
10
11
|
static local<TApi extends object>(api: ClassType<TApi>, apiFactory: ApiFactory): McpApiBinding<TApi>;
|
|
11
12
|
static remote<TApi extends object>(api: ClassType<TApi>, provider: () => TApi): McpApiBinding<TApi>;
|
|
12
13
|
invoke(methodName: string, request: DtoValue, authentication?: InvocationAuthentication): Promise<DtoValue>;
|
|
13
|
-
validateMethod(methodName: string): void;
|
|
14
|
+
validateMethod(methodName: string, policy: AuthorizationService): void;
|
|
14
15
|
}
|
package/src/McpApiBinding.js
CHANGED
|
@@ -7,10 +7,12 @@ class McpApiBinding {
|
|
|
7
7
|
api;
|
|
8
8
|
topology;
|
|
9
9
|
provider;
|
|
10
|
-
|
|
10
|
+
receivingPolicy;
|
|
11
|
+
constructor(api, topology, provider, receivingPolicy) {
|
|
11
12
|
this.api = api;
|
|
12
13
|
this.topology = topology;
|
|
13
14
|
this.provider = provider;
|
|
15
|
+
this.receivingPolicy = receivingPolicy;
|
|
14
16
|
}
|
|
15
17
|
// webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API
|
|
16
18
|
static local(api, apiFactory) {
|
|
@@ -18,7 +20,7 @@ class McpApiBinding {
|
|
|
18
20
|
if (!authentication)
|
|
19
21
|
throw new Error('Local MCP invocation requires explicit authentication.');
|
|
20
22
|
return apiFactory.createInvocationApiClient(api, authentication);
|
|
21
|
-
});
|
|
23
|
+
}, apiFactory.authorizationService());
|
|
22
24
|
}
|
|
23
25
|
// webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API
|
|
24
26
|
static remote(api, provider) {
|
|
@@ -32,13 +34,14 @@ class McpApiBinding {
|
|
|
32
34
|
}
|
|
33
35
|
return method.call(client, request);
|
|
34
36
|
}
|
|
35
|
-
validateMethod(methodName) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
validateMethod(methodName, policy) {
|
|
38
|
+
if (this.topology === 'local' && this.receivingPolicy !== policy) {
|
|
39
|
+
throw new Error('Local MCP visibility and invocation must use the receiving ApiFactory authorizationService().');
|
|
40
|
+
}
|
|
41
|
+
(0, core_util_1.assertEveryEndpointHasAuthMode)(this.api);
|
|
42
|
+
const methods = (0, core_util_1.getAuthMeta)(this.api, methodName)?.methods;
|
|
43
|
+
if (this.topology === 'remote' && !methods?.some((method) => method.kind === 'oidc' || method.kind === 'shared-secret')) {
|
|
44
|
+
throw new Error(`MCP remote binding ${this.api.name}.${methodName} requires oidc(...) or sharedSecret(...) network ingress.`);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
}
|
package/src/McpApiBinding.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpApiBinding.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiBinding.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"McpApiBinding.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiBinding.ts"],"names":[],"mappings":";;;AAAA,oDAAuG;AAWvG,+FAA+F;AAC/F,MAAa,aAAa;IAEF;IACA;IACC;IACA;IAJrB,YACoB,GAAoB,EACpB,QAA4B,EAC3B,QAA6D,EAC7D,eAAsC;QAHvC,QAAG,GAAH,GAAG,CAAiB;QACpB,aAAQ,GAAR,QAAQ,CAAoB;QAC3B,aAAQ,GAAR,QAAQ,CAAqD;QAC7D,oBAAe,GAAf,eAAe,CAAuB;IACxD,CAAC;IAEJ,iGAAiG;IACjG,MAAM,CAAC,KAAK,CACR,GAAoB,EACpB,UAAsB;QAEtB,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,cAAc,EAAE,EAAE;YACtD,IAAI,CAAC,cAAc;gBACf,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;YAC9E,OAAO,UAAU,CAAC,yBAAyB,CAAC,GAAY,EAAE,cAAc,CAAC,CAAC;QAC9E,CAAC,EAAE,UAAU,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,iGAAiG;IACjG,MAAM,CAAC,MAAM,CACT,GAAoB,EACpB,QAAoB;QAEpB,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,CACF,UAAkB,EAClB,OAAiB,EACjB,cAAyC;QAEzC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAmB,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACX,4BAA4B,IAAI,CAAC,GAAG,CAAC,IAAI,mBAAmB,UAAU,IAAI,CAC7E,CAAC;QACN,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAsB,CAAC;IAC7D,CAAC;IAED,cAAc,CAAC,UAAkB,EAAE,MAA4B;QAC3D,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,eAAe,KAAK,MAAM,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,+FAA+F,CAAC,CAAC;QACrH,CAAC;QACD,IAAA,0CAA8B,EAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,IAAA,uBAAW,EAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAC3D,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,MAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe,CAAC,EAAE,CAAC;YAChI,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,UAAU,2DAA2D,CAAC,CAAC;QAClI,CAAC;IACL,CAAC;CACJ;AArDD,sCAqDC","sourcesContent":["import { AuthMode, DtoValue, getAuthMeta, assertEveryEndpointHasAuthMode } from '@webpieces/core-util';\nimport {\n ApiClientProxy,\n ApiFactory,\n AuthorizationService,\n ClassType,\n InvocationAuthentication,\n} from '@webpieces/http-routing';\n\nexport type McpBindingTopology = 'local' | 'remote';\n\n/** Explicit contract-to-invoker binding; MCP never discovers implementations from a router. */\nexport class McpApiBinding<TApi extends object = object> {\n private constructor(\n public readonly api: ClassType<TApi>,\n public readonly topology: McpBindingTopology,\n private readonly provider: (authentication?: InvocationAuthentication) => TApi,\n private readonly receivingPolicy?: AuthorizationService,\n ) {}\n\n // webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API\n static local<TApi extends object>(\n api: ClassType<TApi>,\n apiFactory: ApiFactory,\n ): McpApiBinding<TApi> {\n return new McpApiBinding(api, 'local', (authentication) => {\n if (!authentication)\n throw new Error('Local MCP invocation requires explicit authentication.');\n return apiFactory.createInvocationApiClient(api as never, authentication);\n }, apiFactory.authorizationService());\n }\n\n // webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API\n static remote<TApi extends object>(\n api: ClassType<TApi>,\n provider: () => TApi,\n ): McpApiBinding<TApi> {\n return new McpApiBinding(api, 'remote', provider);\n }\n\n invoke(\n methodName: string,\n request: DtoValue,\n authentication?: InvocationAuthentication,\n ): Promise<DtoValue> {\n const client = this.provider(authentication) as ApiClientProxy;\n const method = client[methodName];\n if (typeof method !== 'function') {\n throw new Error(\n `MCP binding provider for ${this.api.name} has no method '${methodName}'.`,\n );\n }\n return method.call(client, request) as Promise<DtoValue>;\n }\n\n validateMethod(methodName: string, policy: AuthorizationService): void {\n if (this.topology === 'local' && this.receivingPolicy !== policy) {\n throw new Error('Local MCP visibility and invocation must use the receiving ApiFactory authorizationService().');\n }\n assertEveryEndpointHasAuthMode(this.api);\n const methods = getAuthMeta(this.api, methodName)?.methods;\n if (this.topology === 'remote' && !methods?.some((method: AuthMode) => method.kind === 'oidc' || method.kind === 'shared-secret')) {\n throw new Error(`MCP remote binding ${this.api.name}.${methodName} requires oidc(...) or sharedSecret(...) network ingress.`);\n }\n }\n}\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DtoValue } from '@webpieces/core-util';
|
|
2
|
-
import {
|
|
2
|
+
import { AuthorizationService } from '@webpieces/http-routing';
|
|
3
3
|
import { RegisteredMcpTool } from './McpToolRegistry';
|
|
4
4
|
import { VerifiedMcpCredential } from './McpAuth';
|
|
5
5
|
import { McpInvocationContext } from './McpInvocationContext';
|
|
6
6
|
/** Authorize, validate, then issue a local credential and invoke the ordinary filters. No scope or catch. */
|
|
7
7
|
export declare class McpApiDispatcher {
|
|
8
8
|
private readonly schemas;
|
|
9
|
-
call
|
|
9
|
+
call(tool: RegisteredMcpTool, requestDto: DtoValue, credential: VerifiedMcpCredential, invocation: McpInvocationContext, policy: AuthorizationService): Promise<DtoValue>;
|
|
10
10
|
}
|
package/src/McpApiDispatcher.js
CHANGED
|
@@ -9,14 +9,14 @@ const McpInvocationContext_1 = require("./McpInvocationContext");
|
|
|
9
9
|
/** Authorize, validate, then issue a local credential and invoke the ordinary filters. No scope or catch. */
|
|
10
10
|
class McpApiDispatcher {
|
|
11
11
|
schemas = new core_util_1.ApiJsonSchemaValidator();
|
|
12
|
-
async call(tool, requestDto, credential, invocation, policy
|
|
13
|
-
await new McpToolPolicy_1.McpToolPolicy(policy).require(
|
|
12
|
+
async call(tool, requestDto, credential, invocation, policy) {
|
|
13
|
+
await new McpToolPolicy_1.McpToolPolicy(policy).require(tool.authorization, tool.name);
|
|
14
14
|
const inputFailure = this.schemas.validate(tool.inputSchema, requestDto);
|
|
15
15
|
if (inputFailure) {
|
|
16
16
|
throw new core_util_1.ApiBadRequestError(`MCP arguments for ${tool.name} did not match its published input schema: ${inputFailure.message}`, inputFailure.field, inputFailure.message);
|
|
17
17
|
}
|
|
18
18
|
const authentication = tool.binding.topology === 'local'
|
|
19
|
-
? new http_routing_1.InvocationAuthentication(
|
|
19
|
+
? new http_routing_1.InvocationAuthentication(tool.apiClass, tool.methodName)
|
|
20
20
|
: undefined;
|
|
21
21
|
core_context_1.RequestContext.putTrusted(McpInvocationContext_1.MCP_INVOCATION_CONTEXT, invocation);
|
|
22
22
|
return tool.binding.invoke(tool.methodName, requestDto, authentication);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAA4F;AAC5F,0DAAyD;AACzD,
|
|
1
|
+
{"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAA4F;AAC5F,0DAAyD;AACzD,0DAAyF;AACzF,mDAAgD;AAGhD,iEAAsF;AAEtF,6GAA6G;AAC7G,MAAa,gBAAgB;IACR,OAAO,GAAG,IAAI,kCAAsB,EAAE,CAAC;IAExD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,UAAiC,EACjC,UAAgC,EAChC,MAA4B;QAE5B,MAAM,IAAI,6BAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CACnC,IAAI,CAAC,aAAa,EAClB,IAAI,CAAC,IAAI,CACZ,CAAC;QACF,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QACzE,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,8BAAkB,CACxB,qBAAqB,IAAI,CAAC,IAAI,8CAA8C,YAAY,CAAC,OAAO,EAAE,EAClG,YAAY,CAAC,KAAK,EAClB,YAAY,CAAC,OAAO,CACvB,CAAC;QACN,CAAC;QACD,MAAM,cAAc,GAChB,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC7B,CAAC,CAAC,IAAI,uCAAwB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC;YAC9D,CAAC,CAAC,SAAS,CAAC;QACpB,6BAAc,CAAC,UAAU,CAAC,6CAAsB,EAAE,UAAU,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;IAC5E,CAAC;CACJ;AA7BD,4CA6BC","sourcesContent":["import { ApiBadRequestError, ApiJsonSchemaValidator, DtoValue } from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { InvocationAuthentication, AuthorizationService } from '@webpieces/http-routing';\nimport { McpToolPolicy } from './McpToolPolicy';\nimport { RegisteredMcpTool } from './McpToolRegistry';\nimport { VerifiedMcpCredential } from './McpAuth';\nimport { MCP_INVOCATION_CONTEXT, McpInvocationContext } from './McpInvocationContext';\n\n/** Authorize, validate, then issue a local credential and invoke the ordinary filters. No scope or catch. */\nexport class McpApiDispatcher {\n private readonly schemas = new ApiJsonSchemaValidator();\n\n async call(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n credential: VerifiedMcpCredential,\n invocation: McpInvocationContext,\n policy: AuthorizationService,\n ): Promise<DtoValue> {\n await new McpToolPolicy(policy).require(\n tool.authorization,\n tool.name,\n );\n const inputFailure = this.schemas.validate(tool.inputSchema, requestDto);\n if (inputFailure) {\n throw new ApiBadRequestError(\n `MCP arguments for ${tool.name} did not match its published input schema: ${inputFailure.message}`,\n inputFailure.field,\n inputFailure.message,\n );\n }\n const authentication =\n tool.binding.topology === 'local'\n ? new InvocationAuthentication(tool.apiClass, tool.methodName)\n : undefined;\n RequestContext.putTrusted(MCP_INVOCATION_CONTEXT, invocation);\n return tool.binding.invoke(tool.methodName, requestDto, authentication);\n }\n}\n"]}
|
package/src/McpAuth.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { AuthenticatedCaller,
|
|
1
|
+
import { AuthenticatedCaller, AuthorizationService } from '@webpieces/http-routing';
|
|
2
2
|
export declare const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS: number;
|
|
3
3
|
export declare const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS: number;
|
|
4
|
-
export declare const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS: number;
|
|
5
4
|
/** Framework-normalized access-token result. The token representation remains application-owned. */
|
|
6
5
|
export declare class MintedMcpAccessToken {
|
|
7
6
|
readonly token: string;
|
|
@@ -24,12 +23,12 @@ export declare class VerifiedMcpCredential {
|
|
|
24
23
|
readonly scopes: readonly string[];
|
|
25
24
|
/** When current enabled/revoked state and roles were read from the authoritative source. */
|
|
26
25
|
readonly accountValidatedAtEpochSeconds: number;
|
|
27
|
-
/** Application mapping of freshly verified account facts
|
|
26
|
+
/** Application mapping of freshly verified account facts into canonical trusted context. */
|
|
28
27
|
readonly caller: AuthenticatedCaller;
|
|
29
28
|
constructor(subject: string, issuer: string, resource: string, issuedAtEpochSeconds: number, expiresAtEpochSeconds: number, scopes: readonly string[],
|
|
30
29
|
/** When current enabled/revoked state and roles were read from the authoritative source. */
|
|
31
30
|
accountValidatedAtEpochSeconds: number,
|
|
32
|
-
/** Application mapping of freshly verified account facts
|
|
31
|
+
/** Application mapping of freshly verified account facts into canonical trusted context. */
|
|
33
32
|
caller: AuthenticatedCaller);
|
|
34
33
|
/** The canonical application's roles; there is no independent listing-role snapshot. */
|
|
35
34
|
get principalRoles(): readonly string[];
|
|
@@ -43,15 +42,6 @@ export interface McpAccessTokenAuthority<TGrant> {
|
|
|
43
42
|
mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;
|
|
44
43
|
verifyAccessToken(accessToken: string, expectedResource: string): Promise<VerifiedMcpCredential>;
|
|
45
44
|
}
|
|
46
|
-
/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */
|
|
47
|
-
export declare class McpEndpointDescriptor {
|
|
48
|
-
readonly toolName: string;
|
|
49
|
-
readonly apiClassName: string;
|
|
50
|
-
readonly methodName: string;
|
|
51
|
-
constructor(toolName: string, apiClassName: string, methodName: string);
|
|
52
|
-
}
|
|
53
|
-
/** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */
|
|
54
|
-
export type McpEndpointMintRequestFactory<TMintRequest> = (credential: VerifiedMcpCredential, endpoint: McpEndpointDescriptor) => TMintRequest;
|
|
55
45
|
/** RFC 9728-style metadata exposed by the MCP protected resource. */
|
|
56
46
|
export declare class McpProtectedResourceMetadata {
|
|
57
47
|
readonly resource: string;
|
|
@@ -63,9 +53,8 @@ export declare class McpProtectedResourceMetadata {
|
|
|
63
53
|
/**
|
|
64
54
|
* Node MCP server configuration, built with fluent setters: every name sits next to its own value, so
|
|
65
55
|
* no two settings can be swapped, and a future setting is an additive setter rather than a breaking
|
|
66
|
-
* signature change.
|
|
67
|
-
*
|
|
68
|
-
* seams.
|
|
56
|
+
* signature change. The access-token authority verifies MCP credentials; the authorization service
|
|
57
|
+
* is shared with receiving endpoints and document projection.
|
|
69
58
|
*
|
|
70
59
|
* SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is
|
|
71
60
|
* fixed for the lifetime of the process, is the audience of every access token, and is never derived
|
|
@@ -79,13 +68,12 @@ export declare class McpProtectedResourceMetadata {
|
|
|
79
68
|
* so the client's correct response is to re-authenticate and fail again, forever.
|
|
80
69
|
*
|
|
81
70
|
* ```ts
|
|
82
|
-
* const config = new WpMcpServerConfig<MyGrant
|
|
71
|
+
* const config = new WpMcpServerConfig<MyGrant>()
|
|
83
72
|
* .setName('my-server')
|
|
84
73
|
* .setVersion('1.0.0')
|
|
85
74
|
* .setResource('https://api.example.com/mcp')
|
|
86
75
|
* .setAccessTokenAuthority(authority)
|
|
87
|
-
* .
|
|
88
|
-
* .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))
|
|
76
|
+
* .setAuthorizationService(router.authorizationService())
|
|
89
77
|
* .setAuthorizationServers(['https://login.example.com'])
|
|
90
78
|
* .setRequiredScopes(['tools']);
|
|
91
79
|
*
|
|
@@ -93,17 +81,15 @@ export declare class McpProtectedResourceMetadata {
|
|
|
93
81
|
* McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
|
|
94
82
|
* ```
|
|
95
83
|
*/
|
|
96
|
-
export declare class WpMcpServerConfig<TGrant
|
|
84
|
+
export declare class WpMcpServerConfig<TGrant> {
|
|
97
85
|
private nameValue?;
|
|
98
86
|
private versionValue?;
|
|
99
87
|
private resourceValue?;
|
|
100
88
|
private accessTokenAuthorityValue?;
|
|
101
|
-
private
|
|
102
|
-
private endpointMintRequestValue?;
|
|
89
|
+
private authorizationServiceValue?;
|
|
103
90
|
private authorizationServersValue?;
|
|
104
91
|
private requiredScopesValue?;
|
|
105
92
|
private maxAccountValidationAgeSecondsValue;
|
|
106
|
-
private maxEndpointJwtLifetimeSecondsValue;
|
|
107
93
|
/** REQUIRED. The MCP server name reported by `initialize`. */
|
|
108
94
|
setName(name: string): this;
|
|
109
95
|
/** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */
|
|
@@ -124,10 +110,8 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
|
|
|
124
110
|
setResource(resource: string): this;
|
|
125
111
|
/** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */
|
|
126
112
|
setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this;
|
|
127
|
-
/** REQUIRED. The
|
|
128
|
-
|
|
129
|
-
/** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */
|
|
130
|
-
setEndpointMintRequest(factory: McpEndpointMintRequestFactory<TMintRequest>): this;
|
|
113
|
+
/** REQUIRED. The common authorization service for tool projection and invocation. */
|
|
114
|
+
setAuthorizationService(authority: AuthorizationService): this;
|
|
131
115
|
/**
|
|
132
116
|
* REQUIRED. The trusted token issuers. Every entry must be an absolute URL, because each is
|
|
133
117
|
* compared against a token's `issuer` claim and published as an `authorization_servers` entry —
|
|
@@ -141,8 +125,6 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
|
|
|
141
125
|
setRequiredScopes(requiredScopes: readonly string[]): this;
|
|
142
126
|
/** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
|
|
143
127
|
setMaxAccountValidationAgeSeconds(seconds: number): this;
|
|
144
|
-
/** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
|
|
145
|
-
setMaxEndpointJwtLifetimeSeconds(seconds: number): this;
|
|
146
128
|
/**
|
|
147
129
|
* Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a
|
|
148
130
|
* half-built config is repaired in one pass rather than one boot failure at a time.
|
|
@@ -152,12 +134,10 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
|
|
|
152
134
|
get version(): string;
|
|
153
135
|
get resource(): string;
|
|
154
136
|
get accessTokenAuthority(): McpAccessTokenAuthority<TGrant>;
|
|
155
|
-
get
|
|
156
|
-
get endpointMintRequest(): McpEndpointMintRequestFactory<TMintRequest>;
|
|
137
|
+
get authorizationService(): AuthorizationService;
|
|
157
138
|
get authorizationServers(): readonly string[];
|
|
158
139
|
get requiredScopes(): readonly string[];
|
|
159
140
|
get maxAccountValidationAgeSeconds(): number;
|
|
160
|
-
get maxEndpointJwtLifetimeSeconds(): number;
|
|
161
141
|
/**
|
|
162
142
|
* RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from
|
|
163
143
|
* {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path
|
package/src/McpAuth.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WpMcpServerConfig = exports.McpProtectedResourceMetadata = exports.
|
|
3
|
+
exports.WpMcpServerConfig = exports.McpProtectedResourceMetadata = exports.VerifiedMcpCredential = exports.MintedMcpAccessToken = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = void 0;
|
|
4
4
|
exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
5
5
|
exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;
|
|
6
|
-
exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;
|
|
7
6
|
/** Framework-normalized access-token result. The token representation remains application-owned. */
|
|
8
7
|
class MintedMcpAccessToken {
|
|
9
8
|
token;
|
|
@@ -47,7 +46,7 @@ class VerifiedMcpCredential {
|
|
|
47
46
|
constructor(subject, issuer, resource, issuedAtEpochSeconds, expiresAtEpochSeconds, scopes,
|
|
48
47
|
/** When current enabled/revoked state and roles were read from the authoritative source. */
|
|
49
48
|
accountValidatedAtEpochSeconds,
|
|
50
|
-
/** Application mapping of freshly verified account facts
|
|
49
|
+
/** Application mapping of freshly verified account facts into canonical trusted context. */
|
|
51
50
|
caller) {
|
|
52
51
|
this.subject = subject;
|
|
53
52
|
this.issuer = issuer;
|
|
@@ -64,18 +63,6 @@ class VerifiedMcpCredential {
|
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
exports.VerifiedMcpCredential = VerifiedMcpCredential;
|
|
67
|
-
/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */
|
|
68
|
-
class McpEndpointDescriptor {
|
|
69
|
-
toolName;
|
|
70
|
-
apiClassName;
|
|
71
|
-
methodName;
|
|
72
|
-
constructor(toolName, apiClassName, methodName) {
|
|
73
|
-
this.toolName = toolName;
|
|
74
|
-
this.apiClassName = apiClassName;
|
|
75
|
-
this.methodName = methodName;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
exports.McpEndpointDescriptor = McpEndpointDescriptor;
|
|
79
66
|
/** RFC 9728-style metadata exposed by the MCP protected resource. */
|
|
80
67
|
class McpProtectedResourceMetadata {
|
|
81
68
|
resource;
|
|
@@ -92,9 +79,8 @@ exports.McpProtectedResourceMetadata = McpProtectedResourceMetadata;
|
|
|
92
79
|
/**
|
|
93
80
|
* Node MCP server configuration, built with fluent setters: every name sits next to its own value, so
|
|
94
81
|
* no two settings can be swapped, and a future setting is an additive setter rather than a breaking
|
|
95
|
-
* signature change.
|
|
96
|
-
*
|
|
97
|
-
* seams.
|
|
82
|
+
* signature change. The access-token authority verifies MCP credentials; the authorization service
|
|
83
|
+
* is shared with receiving endpoints and document projection.
|
|
98
84
|
*
|
|
99
85
|
* SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is
|
|
100
86
|
* fixed for the lifetime of the process, is the audience of every access token, and is never derived
|
|
@@ -108,13 +94,12 @@ exports.McpProtectedResourceMetadata = McpProtectedResourceMetadata;
|
|
|
108
94
|
* so the client's correct response is to re-authenticate and fail again, forever.
|
|
109
95
|
*
|
|
110
96
|
* ```ts
|
|
111
|
-
* const config = new WpMcpServerConfig<MyGrant
|
|
97
|
+
* const config = new WpMcpServerConfig<MyGrant>()
|
|
112
98
|
* .setName('my-server')
|
|
113
99
|
* .setVersion('1.0.0')
|
|
114
100
|
* .setResource('https://api.example.com/mcp')
|
|
115
101
|
* .setAccessTokenAuthority(authority)
|
|
116
|
-
* .
|
|
117
|
-
* .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))
|
|
102
|
+
* .setAuthorizationService(router.authorizationService())
|
|
118
103
|
* .setAuthorizationServers(['https://login.example.com'])
|
|
119
104
|
* .setRequiredScopes(['tools']);
|
|
120
105
|
*
|
|
@@ -127,12 +112,10 @@ class WpMcpServerConfig {
|
|
|
127
112
|
versionValue;
|
|
128
113
|
resourceValue;
|
|
129
114
|
accessTokenAuthorityValue;
|
|
130
|
-
|
|
131
|
-
endpointMintRequestValue;
|
|
115
|
+
authorizationServiceValue;
|
|
132
116
|
authorizationServersValue;
|
|
133
117
|
requiredScopesValue;
|
|
134
118
|
maxAccountValidationAgeSecondsValue = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;
|
|
135
|
-
maxEndpointJwtLifetimeSecondsValue = exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS;
|
|
136
119
|
/** REQUIRED. The MCP server name reported by `initialize`. */
|
|
137
120
|
setName(name) {
|
|
138
121
|
this.nameValue = this.requireText(name, 'setName');
|
|
@@ -165,17 +148,9 @@ class WpMcpServerConfig {
|
|
|
165
148
|
this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');
|
|
166
149
|
return this;
|
|
167
150
|
}
|
|
168
|
-
/** REQUIRED. The
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
return this;
|
|
172
|
-
}
|
|
173
|
-
/** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */
|
|
174
|
-
setEndpointMintRequest(factory) {
|
|
175
|
-
if (typeof factory !== 'function') {
|
|
176
|
-
throw new Error('WpMcpServerConfig.setEndpointMintRequest(...) requires a function.');
|
|
177
|
-
}
|
|
178
|
-
this.endpointMintRequestValue = factory;
|
|
151
|
+
/** REQUIRED. The common authorization service for tool projection and invocation. */
|
|
152
|
+
setAuthorizationService(authority) {
|
|
153
|
+
this.authorizationServiceValue = this.requirePresent(authority, 'setAuthorizationService');
|
|
179
154
|
return this;
|
|
180
155
|
}
|
|
181
156
|
/**
|
|
@@ -209,11 +184,6 @@ class WpMcpServerConfig {
|
|
|
209
184
|
this.maxAccountValidationAgeSecondsValue = this.requireSeconds(seconds, exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS, 'setMaxAccountValidationAgeSeconds');
|
|
210
185
|
return this;
|
|
211
186
|
}
|
|
212
|
-
/** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
|
|
213
|
-
setMaxEndpointJwtLifetimeSeconds(seconds) {
|
|
214
|
-
this.maxEndpointJwtLifetimeSecondsValue = this.requireSeconds(seconds, exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS, 'setMaxEndpointJwtLifetimeSeconds');
|
|
215
|
-
return this;
|
|
216
|
-
}
|
|
217
187
|
/**
|
|
218
188
|
* Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a
|
|
219
189
|
* half-built config is repaired in one pass rather than one boot failure at a time.
|
|
@@ -229,11 +199,8 @@ class WpMcpServerConfig {
|
|
|
229
199
|
if (this.accessTokenAuthorityValue === undefined) {
|
|
230
200
|
missing.push('setAccessTokenAuthority(...)');
|
|
231
201
|
}
|
|
232
|
-
if (this.
|
|
233
|
-
missing.push('
|
|
234
|
-
}
|
|
235
|
-
if (this.endpointMintRequestValue === undefined) {
|
|
236
|
-
missing.push('setEndpointMintRequest(...)');
|
|
202
|
+
if (this.authorizationServiceValue === undefined) {
|
|
203
|
+
missing.push('setAuthorizationService(...)');
|
|
237
204
|
}
|
|
238
205
|
if (this.authorizationServersValue === undefined) {
|
|
239
206
|
missing.push('setAuthorizationServers(...)');
|
|
@@ -256,11 +223,8 @@ class WpMcpServerConfig {
|
|
|
256
223
|
get accessTokenAuthority() {
|
|
257
224
|
return this.read(this.accessTokenAuthorityValue, 'setAccessTokenAuthority');
|
|
258
225
|
}
|
|
259
|
-
get
|
|
260
|
-
return this.read(this.
|
|
261
|
-
}
|
|
262
|
-
get endpointMintRequest() {
|
|
263
|
-
return this.read(this.endpointMintRequestValue, 'setEndpointMintRequest');
|
|
226
|
+
get authorizationService() {
|
|
227
|
+
return this.read(this.authorizationServiceValue, 'setAuthorizationService');
|
|
264
228
|
}
|
|
265
229
|
get authorizationServers() {
|
|
266
230
|
return this.read(this.authorizationServersValue, 'setAuthorizationServers');
|
|
@@ -271,9 +235,6 @@ class WpMcpServerConfig {
|
|
|
271
235
|
get maxAccountValidationAgeSeconds() {
|
|
272
236
|
return this.maxAccountValidationAgeSecondsValue;
|
|
273
237
|
}
|
|
274
|
-
get maxEndpointJwtLifetimeSeconds() {
|
|
275
|
-
return this.maxEndpointJwtLifetimeSecondsValue;
|
|
276
|
-
}
|
|
277
238
|
/**
|
|
278
239
|
* RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from
|
|
279
240
|
* {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path
|