nest-graphql-mcp 0.0.0-stage → 0.1.0
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/LICENSE.md +9 -0
- package/README.md +214 -3
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +15 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/mcp.controller.d.ts +8 -0
- package/dist/mcp.controller.js +89 -0
- package/dist/mcp.decorator.d.ts +5 -0
- package/dist/mcp.decorator.js +17 -0
- package/dist/mcp.service.d.ts +20 -0
- package/dist/mcp.service.js +189 -0
- package/dist/nest-graphql-mcp.module.d.ts +5 -0
- package/dist/nest-graphql-mcp.module.js +34 -0
- package/dist/src/constants.d.ts +6 -0
- package/dist/src/constants.js +15 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +21 -0
- package/dist/src/mcp.controller.d.ts +8 -0
- package/dist/src/mcp.controller.js +89 -0
- package/dist/src/mcp.decorator.d.ts +5 -0
- package/dist/src/mcp.decorator.js +17 -0
- package/dist/src/mcp.service.d.ts +20 -0
- package/dist/src/mcp.service.js +189 -0
- package/dist/src/nest-graphql-mcp.module.d.ts +5 -0
- package/dist/src/nest-graphql-mcp.module.js +34 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/types.js +2 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +2 -0
- package/package.json +67 -4
- package/src/constants.ts +14 -0
- package/src/index.ts +5 -0
- package/src/mcp.controller.ts +75 -0
- package/src/mcp.decorator.ts +14 -0
- package/src/mcp.service.ts +228 -0
- package/src/nest-graphql-mcp.module.ts +23 -0
- package/src/types.ts +19 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chase McManning
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,214 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
This
|
|
1
|
+
# Nest GraphQL MCP Module
|
|
2
|
+
|
|
3
|
+
This module provides integration between NestJS, GraphQL, and the Model Context Protocol (MCP).
|
|
4
|
+
|
|
5
|
+
If you have a mature GraphQL environment, this module allows you to expose specific queries and mutations to AI agents as MCP tools by using your existing schema types and documentation directly.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install nest-graphql-mcp @modelcontextprotocol/sdk zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Module Registration
|
|
14
|
+
|
|
15
|
+
Import `NestGraphqlMcpModule.forRoot()` in your root `AppModule`:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Module } from '@nestjs/common';
|
|
19
|
+
import { NestGraphqlMcpModule } from 'nest-graphql-mcp';
|
|
20
|
+
|
|
21
|
+
@Module({
|
|
22
|
+
imports: [
|
|
23
|
+
NestGraphqlMcpModule.forRoot({
|
|
24
|
+
path: '/mcp/stateless', // Endpoint for AI agents
|
|
25
|
+
}),
|
|
26
|
+
// ... GraphQLModule import (see Code-First or Schema-First setup below)
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
export class AppModule {}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Code-First Approach
|
|
33
|
+
|
|
34
|
+
Use TypeScript decorators to generate the GraphQL schema and register the MCP directive.
|
|
35
|
+
|
|
36
|
+
Pass `MCP_DIRECTIVE_DEFINITION` to buildSchemaOptions.directives:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Module } from '@nestjs/common';
|
|
40
|
+
import { GraphQLModule } from '@nestjs/graphql';
|
|
41
|
+
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
|
42
|
+
import { NestGraphqlMcpModule, MCP_DIRECTIVE_DEFINITION } from 'nest-graphql-mcp';
|
|
43
|
+
|
|
44
|
+
@Module({
|
|
45
|
+
imports: [
|
|
46
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
47
|
+
driver: ApolloDriver,
|
|
48
|
+
autoSchemaFile: true,
|
|
49
|
+
buildSchemaOptions: {
|
|
50
|
+
directives: [MCP_DIRECTIVE_DEFINITION],
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
NestGraphqlMcpModule.forRoot(),
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
export class AppModule {}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Attach the `@ExposeMcp()` decorator to any GraphQL Query or Mutation you want to expose to AI agents. You can optionally pass agent-specific prompt instructions.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Resolver, Query, Args } from '@nestjs/graphql';
|
|
63
|
+
import { UseGuards } from '@nestjs/common';
|
|
64
|
+
import { ExposeMcp } from 'nest-graphql-mcp';
|
|
65
|
+
import { AuthGuard } from './auth.guard';
|
|
66
|
+
import { User } from './user.model';
|
|
67
|
+
|
|
68
|
+
@Resolver(() => User)
|
|
69
|
+
export class UserResolver {
|
|
70
|
+
@Query(() => User, {
|
|
71
|
+
description: 'Fetches user details by ID.',
|
|
72
|
+
})
|
|
73
|
+
@ExposeMcp('Prefer looking up users by UUID. Do not call in a loop for bulk queries.')
|
|
74
|
+
@UseGuards(AuthGuard)
|
|
75
|
+
async userById(@Args('id') id: string) {
|
|
76
|
+
return this.userService.findById(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Query(() => [User])
|
|
80
|
+
// Standard GQL query hidden from MCP tools because @ExposeMcp() is omitted
|
|
81
|
+
async internalAdminSearch() {
|
|
82
|
+
return this.userService.findAll();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Schema-First Approach
|
|
88
|
+
|
|
89
|
+
In the Schema-First approach, you define your GraphQL types and directives inside .graphql SDL files. This module automatically reads the compiled GraphQL AST at startup to discover fields annotated with `@mcp`.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Module } from '@nestjs/common';
|
|
93
|
+
import { GraphQLModule } from '@nestjs/graphql';
|
|
94
|
+
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
|
95
|
+
import { NestGraphqlMcpModule, MCP_DIRECTIVE_DEFINITION } from 'nest-graphql-mcp';
|
|
96
|
+
|
|
97
|
+
@Module({
|
|
98
|
+
imports: [
|
|
99
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
100
|
+
driver: ApolloDriver,
|
|
101
|
+
typePaths: ['./**/*.graphql'],
|
|
102
|
+
definitions: {
|
|
103
|
+
path: join(process.cwd(), 'src/graphql.ts'),
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
NestGraphqlMcpModule.forRoot(),
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
export class AppModule {}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Add the `@mcp` directive definition to your .graphql schema file and annotate fields directly:
|
|
113
|
+
|
|
114
|
+
```graphql
|
|
115
|
+
" Directive to expose operations as MCP Tools to AI agents. "
|
|
116
|
+
directive @mcp(instructions: String) on FIELD_DEFINITION
|
|
117
|
+
|
|
118
|
+
type User {
|
|
119
|
+
id: ID!
|
|
120
|
+
name: String!
|
|
121
|
+
email: String!
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type Query {
|
|
125
|
+
me: User @mcp
|
|
126
|
+
|
|
127
|
+
# Exposing to MCP with custom agent instructions
|
|
128
|
+
userById(id: ID!): User @mcp(instructions: "Prefer looking up users by UUID. Do not call in a loop for bulk queries.")
|
|
129
|
+
|
|
130
|
+
# Hidden from MCP tools
|
|
131
|
+
internalAdminSearch: [User!]!
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Import your `.graphql` files as usual. No special directive configuration is needed in Typescript.
|
|
136
|
+
|
|
137
|
+
## Authentication & Context Forwarding
|
|
138
|
+
|
|
139
|
+
When an AI agent calls an MCP tool over HTTP, `nest-graphql-mcp` forwards the incoming `Authorization` header directly into GraphQL's execution context:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
[AI Agent]
|
|
143
|
+
│
|
|
144
|
+
│ HTTP POST /mcp/stateless
|
|
145
|
+
│ Header: Authorization: Bearer <SERVICE_ACCOUNT_JWT>
|
|
146
|
+
▼
|
|
147
|
+
[nest-graphql-mcp]
|
|
148
|
+
│
|
|
149
|
+
│ Executes query with contextValue: { req: { headers: { authorization } } }
|
|
150
|
+
▼
|
|
151
|
+
[NestJS Resolvers / @UseGuards()]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This ensures your existing NestJS Guards, Role Checks, and Field-level permissions continue to execute for AI service accounts.
|
|
155
|
+
|
|
156
|
+
## Agent Setup & Testing
|
|
157
|
+
|
|
158
|
+
### Cursor / Claude / Standard MCP Setup
|
|
159
|
+
|
|
160
|
+
Add the endpoint and service account JWT to your mcp.json config:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
{
|
|
164
|
+
"mcpServers": {
|
|
165
|
+
"nestjs-api": {
|
|
166
|
+
"url": "http://localhost:3000/mcp/stateless",
|
|
167
|
+
"transport": "http",
|
|
168
|
+
"headers": {
|
|
169
|
+
"Authorization": "Bearer YOUR_SERVICE_ACCOUNT_JWT"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Inspectiing Tools via Curl
|
|
177
|
+
|
|
178
|
+
You can verify that exposed tools and JSON Schemas are generated correctly by sending a JSON-RPC request:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
# List Tools
|
|
182
|
+
curl -X POST http://localhost:3000/api/mcp/stateless \
|
|
183
|
+
-H "Content-Type: application/json" \
|
|
184
|
+
-H "Accept: application/json, text/event-stream" \
|
|
185
|
+
-H "Authorization: Bearer YOUR_SERVICE_ACCOUNT_JWT" \
|
|
186
|
+
-d '{
|
|
187
|
+
"jsonrpc": "2.0",
|
|
188
|
+
"id": 1,
|
|
189
|
+
"method": "tools/list"
|
|
190
|
+
}'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
# Execute Tool
|
|
195
|
+
curl -s -X POST http://localhost:3000/api/mcp/stateless \
|
|
196
|
+
-H "Content-Type: application/json" \
|
|
197
|
+
-H "Accept: application/json, text/event-stream" \
|
|
198
|
+
-H "Authorization: Bearer YOUR_SERVICE_ACCOUNT_JWT" \
|
|
199
|
+
-d '{
|
|
200
|
+
"jsonrpc": "2.0",
|
|
201
|
+
"id": 2,
|
|
202
|
+
"method": "tools/call",
|
|
203
|
+
"params": {
|
|
204
|
+
"name": "query_getUser",
|
|
205
|
+
"arguments": {
|
|
206
|
+
"id": "usr_123"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT, see the included [LICENSE](LICENSE.md) file.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MCP_DIRECTIVE_DEFINITION = exports.MCP_OPTIONS = void 0;
|
|
4
|
+
const graphql_1 = require("graphql");
|
|
5
|
+
exports.MCP_OPTIONS = Symbol('MCP_OPTIONS');
|
|
6
|
+
/**
|
|
7
|
+
* GraphQL directive definition for exposing fields as MCP.
|
|
8
|
+
*/
|
|
9
|
+
exports.MCP_DIRECTIVE_DEFINITION = new graphql_1.GraphQLDirective({
|
|
10
|
+
name: 'mcp',
|
|
11
|
+
locations: [graphql_1.DirectiveLocation.FIELD_DEFINITION],
|
|
12
|
+
args: {
|
|
13
|
+
instructions: { type: graphql_1.GraphQLString },
|
|
14
|
+
},
|
|
15
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./constants"), exports);
|
|
18
|
+
__exportStar(require("./mcp.decorator"), exports);
|
|
19
|
+
__exportStar(require("./mcp.service"), exports);
|
|
20
|
+
__exportStar(require("./nest-graphql-mcp.module"), exports);
|
|
21
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Request, type Response } from 'express';
|
|
2
|
+
import { McpService } from './mcp.service';
|
|
3
|
+
export declare class McpController {
|
|
4
|
+
private readonly mcpService;
|
|
5
|
+
constructor(mcpService: McpService);
|
|
6
|
+
private transport;
|
|
7
|
+
handleStatelessMcp(req: Request, res: Response): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.McpController = void 0;
|
|
16
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const mcp_service_1 = require("./mcp.service");
|
|
19
|
+
let McpController = class McpController {
|
|
20
|
+
mcpService;
|
|
21
|
+
// Store active sessions mapped by their session ID
|
|
22
|
+
// private readonly sessions = new Map<string, StreamableHTTPServerTransport>();
|
|
23
|
+
constructor(mcpService) {
|
|
24
|
+
this.mcpService = mcpService;
|
|
25
|
+
}
|
|
26
|
+
// The single McpServer instance supports one transport at a time, so track the
|
|
27
|
+
// active transport and close it before reconnecting on the next request.
|
|
28
|
+
// ponytail: one in-flight request at a time per endpoint; per-request McpServer
|
|
29
|
+
// instances (or sessions) when concurrent stateless connections are needed.
|
|
30
|
+
transport;
|
|
31
|
+
// @All()
|
|
32
|
+
// async handleMcp(@Req() req: Request, @Res() res: Response) {
|
|
33
|
+
// const authHeader = req.headers.authorization;
|
|
34
|
+
// if (!authHeader) {
|
|
35
|
+
// throw new UnauthorizedException('Missing Authorization header');
|
|
36
|
+
// }
|
|
37
|
+
// const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
38
|
+
// // Resume existing session if mcp-session-id header is provided
|
|
39
|
+
// if (sessionId && this.sessions.has(sessionId)) {
|
|
40
|
+
// const transport = this.sessions.get(sessionId)!;
|
|
41
|
+
// await transport.handleRequest(req, res, req.body);
|
|
42
|
+
// return;
|
|
43
|
+
// }
|
|
44
|
+
// const transport = new StreamableHTTPServerTransport({
|
|
45
|
+
// sessionIdGenerator: () => randomUUID(),
|
|
46
|
+
// onsessioninitialized: (id) => this.sessions.set(id, transport),
|
|
47
|
+
// });
|
|
48
|
+
// transport.onclose = () => {
|
|
49
|
+
// if (transport.sessionId) {
|
|
50
|
+
// this.sessions.delete(transport.sessionId);
|
|
51
|
+
// }
|
|
52
|
+
// };
|
|
53
|
+
// // Connect transport to the underlying McpServer and handle request
|
|
54
|
+
// await this.mcpService.getMcpServer().connect(transport);
|
|
55
|
+
// await transport.handleRequest(req, res, req.body);
|
|
56
|
+
// }
|
|
57
|
+
async handleStatelessMcp(req, res) {
|
|
58
|
+
// TODO: This is if we don't need in-memory session tracking acrosst urns
|
|
59
|
+
// (e.g. token is validated on every request). Probably the preferred route.
|
|
60
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
61
|
+
// We don't need to persist state between requests.
|
|
62
|
+
sessionIdGenerator: undefined,
|
|
63
|
+
});
|
|
64
|
+
res.on('close', () => {
|
|
65
|
+
transport.close();
|
|
66
|
+
});
|
|
67
|
+
// Close the previous transport (no-op if already closed) so the shared
|
|
68
|
+
// server can attach to this request's transport.
|
|
69
|
+
if (this.transport) {
|
|
70
|
+
await this.transport.close();
|
|
71
|
+
}
|
|
72
|
+
this.transport = transport;
|
|
73
|
+
await this.mcpService.getMcpServer().connect(transport);
|
|
74
|
+
await transport.handleRequest(req, res, req.body);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
exports.McpController = McpController;
|
|
78
|
+
__decorate([
|
|
79
|
+
(0, common_1.All)('stateless'),
|
|
80
|
+
__param(0, (0, common_1.Req)()),
|
|
81
|
+
__param(1, (0, common_1.Res)()),
|
|
82
|
+
__metadata("design:type", Function),
|
|
83
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
84
|
+
__metadata("design:returntype", Promise)
|
|
85
|
+
], McpController.prototype, "handleStatelessMcp", null);
|
|
86
|
+
exports.McpController = McpController = __decorate([
|
|
87
|
+
(0, common_1.Controller)('mcp'),
|
|
88
|
+
__metadata("design:paramtypes", [mcp_service_1.McpService])
|
|
89
|
+
], McpController);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExposeMcp = void 0;
|
|
4
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
5
|
+
/**
|
|
6
|
+
* Whitelists a GraphQL Query or Mutation for MCP and optionally adds custom
|
|
7
|
+
* instructions specifically for AI agents.
|
|
8
|
+
*/
|
|
9
|
+
const ExposeMcp = (agentInstructions) => {
|
|
10
|
+
if (agentInstructions) {
|
|
11
|
+
// Escape quotes to prevent directive string syntax errors
|
|
12
|
+
const sanitized = agentInstructions.replace(/"/g, '\\"');
|
|
13
|
+
return (0, graphql_1.Directive)(`@mcp(instructions: "${sanitized}")`);
|
|
14
|
+
}
|
|
15
|
+
return (0, graphql_1.Directive)('@mcp');
|
|
16
|
+
};
|
|
17
|
+
exports.ExposeMcp = ExposeMcp;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
3
|
+
import { ModuleRef } from '@nestjs/core';
|
|
4
|
+
import { type GraphqlMcpModuleOptions } from './types';
|
|
5
|
+
export declare class McpService implements OnModuleInit {
|
|
6
|
+
private readonly moduleRef;
|
|
7
|
+
private readonly options;
|
|
8
|
+
private readonly mcpServer;
|
|
9
|
+
private schemaHost;
|
|
10
|
+
constructor(moduleRef: ModuleRef, options: GraphqlMcpModuleOptions);
|
|
11
|
+
onModuleInit(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Helper to parse the `@mcp` directive and its `instructions` argument from field AST.
|
|
14
|
+
*/
|
|
15
|
+
private getMcpDirectiveConfig;
|
|
16
|
+
private mapGraphQLTypeToZod;
|
|
17
|
+
private buildSelectionSet;
|
|
18
|
+
getMcpServer(): McpServer;
|
|
19
|
+
private registerObjectTypeFields;
|
|
20
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.McpService = void 0;
|
|
16
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const core_1 = require("@nestjs/core");
|
|
19
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
20
|
+
const graphql_2 = require("graphql");
|
|
21
|
+
const zod_1 = require("zod");
|
|
22
|
+
const constants_1 = require("./constants");
|
|
23
|
+
let McpService = class McpService {
|
|
24
|
+
moduleRef;
|
|
25
|
+
options;
|
|
26
|
+
mcpServer;
|
|
27
|
+
schemaHost;
|
|
28
|
+
constructor(moduleRef, options) {
|
|
29
|
+
this.moduleRef = moduleRef;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.mcpServer = new mcp_js_1.McpServer({
|
|
32
|
+
name: this.options.serverName || 'nestjs-mcp-server',
|
|
33
|
+
version: this.options.serverVersion || '1.0.0',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
onModuleInit() {
|
|
37
|
+
this.schemaHost = this.moduleRef.get(graphql_1.GraphQLSchemaHost, { strict: false });
|
|
38
|
+
const { schema } = this.schemaHost;
|
|
39
|
+
if (!schema) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const queryType = schema.getQueryType();
|
|
43
|
+
if (queryType) {
|
|
44
|
+
this.registerObjectTypeFields(queryType, 'query');
|
|
45
|
+
}
|
|
46
|
+
const mutationType = schema.getMutationType();
|
|
47
|
+
if (mutationType) {
|
|
48
|
+
this.registerObjectTypeFields(mutationType, 'mutation');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Helper to parse the `@mcp` directive and its `instructions` argument from field AST.
|
|
53
|
+
*/
|
|
54
|
+
getMcpDirectiveConfig(field) {
|
|
55
|
+
const directives = field.astNode?.directives;
|
|
56
|
+
if (!directives) {
|
|
57
|
+
return { isExposed: false };
|
|
58
|
+
}
|
|
59
|
+
const mcpDirective = directives.find((d) => d.name.value === 'mcp');
|
|
60
|
+
if (!mcpDirective) {
|
|
61
|
+
return { isExposed: false };
|
|
62
|
+
}
|
|
63
|
+
let instructions;
|
|
64
|
+
const arg = mcpDirective.arguments?.find((a) => a.name.value === 'instructions');
|
|
65
|
+
if (arg && arg.value.kind === 'StringValue') {
|
|
66
|
+
instructions = arg.value.value;
|
|
67
|
+
}
|
|
68
|
+
return { isExposed: true, instructions };
|
|
69
|
+
}
|
|
70
|
+
mapGraphQLTypeToZod(type) {
|
|
71
|
+
const isNonNull = (0, graphql_2.isNonNullType)(type);
|
|
72
|
+
const unwrappedType = isNonNull
|
|
73
|
+
? type.ofType
|
|
74
|
+
: type;
|
|
75
|
+
let schema;
|
|
76
|
+
if ((0, graphql_2.isScalarType)(unwrappedType)) {
|
|
77
|
+
switch (unwrappedType.name) {
|
|
78
|
+
case 'Int':
|
|
79
|
+
case 'Float':
|
|
80
|
+
schema = zod_1.z.number();
|
|
81
|
+
break;
|
|
82
|
+
case 'Boolean':
|
|
83
|
+
schema = zod_1.z.boolean();
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
schema = zod_1.z.string();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
schema = zod_1.z.any();
|
|
91
|
+
}
|
|
92
|
+
return isNonNull ? schema : schema.optional();
|
|
93
|
+
}
|
|
94
|
+
buildSelectionSet(returnType) {
|
|
95
|
+
let namedType = returnType;
|
|
96
|
+
while ('ofType' in namedType) {
|
|
97
|
+
namedType = namedType.ofType;
|
|
98
|
+
}
|
|
99
|
+
if ((0, graphql_2.isObjectType)(namedType)) {
|
|
100
|
+
const fields = namedType.getFields();
|
|
101
|
+
const scalarFields = Object.keys(fields).filter((key) => {
|
|
102
|
+
let fType = fields[key].type;
|
|
103
|
+
while ('ofType' in fType) {
|
|
104
|
+
fType = fType.ofType;
|
|
105
|
+
}
|
|
106
|
+
return (0, graphql_2.isScalarType)(fType);
|
|
107
|
+
});
|
|
108
|
+
return scalarFields.join('\n');
|
|
109
|
+
}
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
getMcpServer() {
|
|
113
|
+
return this.mcpServer;
|
|
114
|
+
}
|
|
115
|
+
registerObjectTypeFields(objectType, operationType) {
|
|
116
|
+
const fields = objectType.getFields();
|
|
117
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
118
|
+
// Check for @mcp/@ExposeMcp directives and parse instructions
|
|
119
|
+
const mcpConfig = this.getMcpDirectiveConfig(field);
|
|
120
|
+
if (!mcpConfig.isExposed) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const toolName = `${operationType}_${fieldName}`;
|
|
124
|
+
// Build an additive description (Base GraphQL + Agent Context)
|
|
125
|
+
const baseDescription = field.description || `Executes the ${fieldName} ${operationType}.`;
|
|
126
|
+
const finalDescription = mcpConfig.instructions
|
|
127
|
+
? `${baseDescription}\n\n[Agent Context]: ${mcpConfig.instructions}`
|
|
128
|
+
: baseDescription;
|
|
129
|
+
// Build input schema
|
|
130
|
+
const zodShape = {};
|
|
131
|
+
for (const arg of field.args) {
|
|
132
|
+
zodShape[arg.name] = this.mapGraphQLTypeToZod(arg.type);
|
|
133
|
+
}
|
|
134
|
+
this.mcpServer.registerTool(toolName, {
|
|
135
|
+
title: fieldName,
|
|
136
|
+
description: finalDescription,
|
|
137
|
+
inputSchema: zodShape,
|
|
138
|
+
}, async (args, extra) => {
|
|
139
|
+
const selectionSet = this.buildSelectionSet(field.type);
|
|
140
|
+
const selectionBlock = selectionSet ? `{ ${selectionSet} }` : '';
|
|
141
|
+
const varDefs = field.args
|
|
142
|
+
.map((a) => `$${a.name}: ${a.type.toString()}`)
|
|
143
|
+
.join(', ');
|
|
144
|
+
const varArgs = field.args
|
|
145
|
+
.map((a) => `${a.name}: $${a.name}`)
|
|
146
|
+
.join(', ');
|
|
147
|
+
const queryString = `
|
|
148
|
+
${operationType} ${fieldName}McpWrapper${varDefs ? `(${varDefs})` : ''} {
|
|
149
|
+
${fieldName}${varArgs ? `(${varArgs})` : ''} ${selectionBlock}
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
const req = extra?.request;
|
|
153
|
+
const authHeader = req?.headers?.['authorization'];
|
|
154
|
+
const result = await (0, graphql_2.graphql)({
|
|
155
|
+
schema: this.schemaHost.schema,
|
|
156
|
+
source: queryString,
|
|
157
|
+
variableValues: args,
|
|
158
|
+
contextValue: {
|
|
159
|
+
req: {
|
|
160
|
+
headers: { authorization: authHeader },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
if (result.errors) {
|
|
165
|
+
return {
|
|
166
|
+
isError: true,
|
|
167
|
+
content: [
|
|
168
|
+
{ type: 'text', text: JSON.stringify(result.errors, null, 2) },
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: JSON.stringify(result.data?.[fieldName], null, 2),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
exports.McpService = McpService;
|
|
185
|
+
exports.McpService = McpService = __decorate([
|
|
186
|
+
(0, common_1.Injectable)(),
|
|
187
|
+
__param(1, (0, common_1.Inject)(constants_1.MCP_OPTIONS)),
|
|
188
|
+
__metadata("design:paramtypes", [core_1.ModuleRef, Object])
|
|
189
|
+
], McpService);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var NestGraphqlMcpModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.NestGraphqlMcpModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
const mcp_controller_1 = require("./mcp.controller");
|
|
14
|
+
const mcp_service_1 = require("./mcp.service");
|
|
15
|
+
let NestGraphqlMcpModule = NestGraphqlMcpModule_1 = class NestGraphqlMcpModule {
|
|
16
|
+
static forRoot(options = {}) {
|
|
17
|
+
return {
|
|
18
|
+
module: NestGraphqlMcpModule_1,
|
|
19
|
+
controllers: [mcp_controller_1.McpController],
|
|
20
|
+
providers: [
|
|
21
|
+
{
|
|
22
|
+
provide: constants_1.MCP_OPTIONS,
|
|
23
|
+
useValue: options,
|
|
24
|
+
},
|
|
25
|
+
mcp_service_1.McpService,
|
|
26
|
+
],
|
|
27
|
+
exports: [mcp_service_1.McpService, constants_1.MCP_OPTIONS],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
exports.NestGraphqlMcpModule = NestGraphqlMcpModule;
|
|
32
|
+
exports.NestGraphqlMcpModule = NestGraphqlMcpModule = NestGraphqlMcpModule_1 = __decorate([
|
|
33
|
+
(0, common_1.Module)({})
|
|
34
|
+
], NestGraphqlMcpModule);
|