Skip to content

Commit a9fe40e

Browse files
feat(chore): access-control interceptor support for MCP endpoint
access-control interceptor support for MCP endpoint GH-1
1 parent 9f21f07 commit a9fe40e

File tree

7 files changed

+105
-7
lines changed

7 files changed

+105
-7
lines changed

README.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ The schema field allows defining a Zod-based validation schema for tool input pa
5454
To use hooks with MCP tools, follow the provider-based approach:
5555

5656
Step 1: Create a hook provider:
57-
```typescript
57+
```ts
5858
// src/providers/my-hook.provider.ts
5959
export class MyHookProvider implements Provider<McpHookFunction> {
6060
constructor(@inject(LOGGER.LOGGER_INJECT) private logger: ILogger) {}
@@ -66,7 +66,7 @@ The schema field allows defining a Zod-based validation schema for tool input pa
6666
}
6767
```
6868
Step 2: Add binding key to McpHookBindings:
69-
```typescript
69+
```ts
7070
// src/keys.ts
7171
export namespace McpHookBindings {
7272
export const MY_HOOK = BindingKey.create<McpHookFunction>('hooks.mcp.myHook');
@@ -77,10 +77,65 @@ The schema field allows defining a Zod-based validation schema for tool input pa
7777
this.bind(McpHookBindings.MY_HOOK).toProvider(MyHookProvider);
7878
```
7979
Step 4: Use in decorator:
80-
```typescript
80+
```ts
8181
@mcpTool({
8282
name: 'my-tool',
8383
description: 'my-description'
8484
preHookBinding: McpHookBindings.MY_HOOK,
8585
postHookBinding: 'hooks.mcp.myOtherHook' // or string binding key
8686
})
87+
```
88+
## MCP Access Control
89+
By default, any authenticated user can call the `/mcp` endpoint.
90+
To restrict which users can access MCP functionality, this extension supports adding a custom LoopBack 4 interceptor.
91+
92+
Create an interceptor that validates the authenticated user before the request reaches the MCP controller.
93+
94+
```ts
95+
import {
96+
InvocationContext,
97+
InvocationResult,
98+
Provider,
99+
ValueOrPromise,
100+
inject,
101+
interceptor,
102+
} from '@loopback/core';
103+
import {HttpErrors} from '@loopback/rest';
104+
import {AuthenticationBindings, IAuthUser} from 'loopback4-authentication';
105+
106+
@interceptor()
107+
export class McpAccessInterceptor implements Provider<Interceptor> {
108+
constructor(
109+
@inject(AuthenticationBindings.CURRENT_USER, {optional: true})
110+
private readonly currentUser: IAuthUser,
111+
) {}
112+
113+
value() {
114+
return this.intercept.bind(this);
115+
}
116+
117+
intercept(
118+
invocationCtx: InvocationContext,
119+
next: () => ValueOrPromise<InvocationResult>,
120+
): ValueOrPromise<InvocationResult> {
121+
if (!this.currentUser) {
122+
throw new HttpErrors.Unauthorized('User not authenticated');
123+
}
124+
125+
const user = this.currentUser;
126+
127+
// Example rule: only admins or users with `access-mcp` permission
128+
if (user.role !== 'admin' && !user.permissions?.includes('access-mcp')) {
129+
throw new HttpErrors.Forbidden(
130+
`User ${user.username} is not allowed to access MCP`,
131+
);
132+
}
133+
134+
return next();
135+
}
136+
}
137+
```
138+
Bind it in your `application.ts`:
139+
```ts
140+
this.bind(McpBindings.ACCESS_INTERCEPTOR).toProvider(McpAccessInterceptor);
141+
```

src/controllers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './mcp-controller.controller';
1+
export * from './mcp.controller';

src/controllers/mcp-controller.controller.ts renamed to src/controllers/mcp.controller.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import {inject, service} from '@loopback/core';
1+
import {inject, intercept, service} from '@loopback/core';
22
import {post, Request, Response, RestBindings} from '@loopback/rest';
33
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
44
import {CONTENT_TYPE, ILogger, LOGGER, STATUS_CODE} from '@sourceloop/core';
55
import {authenticate, STRATEGY} from 'loopback4-authentication';
66
import {authorize} from 'loopback4-authorization';
77
import {McpServerFactory} from '../services';
8+
import {McpBindings} from '../keys';
89

910
export class McpController {
1011
constructor(
@@ -18,8 +19,9 @@ export class McpController {
1819
passReqToCallback: true,
1920
})
2021
@authorize({
21-
permissions: ['mcp.access'],
22+
permissions: ['*'],
2223
})
24+
@intercept(McpBindings.ACCESS_INTERCEPTOR)
2325
@post('/mcp', {
2426
summary: 'MCP HTTP Message',
2527
description: 'Handle MCP message via StreamableHTTP transport',

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './component';
22
export * from './types';
33
export * from './decorators';
44
export * from './observers';
5+
export * from './interceptors';
6+
export * from './keys';

src/interceptors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './mcp-access.interceptor';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {
2+
Interceptor,
3+
InvocationContext,
4+
InvocationResult,
5+
Provider,
6+
ValueOrPromise,
7+
inject,
8+
} from '@loopback/core';
9+
import {HttpErrors} from '@loopback/rest';
10+
import {IAuthUserWithPermissions} from '@sourceloop/core';
11+
import {AuthenticationBindings} from 'loopback4-authentication';
12+
13+
export class McpAccessInterceptor implements Provider<Interceptor> {
14+
constructor(
15+
@inject(AuthenticationBindings.CURRENT_USER)
16+
private readonly currentUser: IAuthUserWithPermissions,
17+
) {}
18+
19+
value() {
20+
return this.intercept.bind(this);
21+
}
22+
23+
intercept(
24+
invocationCtx: InvocationContext,
25+
next: () => ValueOrPromise<InvocationResult>,
26+
): ValueOrPromise<InvocationResult> {
27+
if (!this.currentUser) {
28+
throw new HttpErrors.Unauthorized('User not authenticated');
29+
}
30+
return next();
31+
}
32+
}

src/keys.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
export namespace McpComponentBindings {}
1+
import {BindingKey, Interceptor} from '@loopback/core';
2+
3+
export namespace McpBindings {
4+
export const ACCESS_INTERCEPTOR = BindingKey.create<Interceptor>(
5+
'mcp.access.interceptor',
6+
);
7+
}

0 commit comments

Comments
 (0)