@nestjs-mcp/server 0.1.0-alpha.4
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/.copilotignore +38 -0
- package/.devcontainer/Dockerfile.dev +28 -0
- package/.devcontainer/devcontainer.json +56 -0
- package/.devcontainer/docker-compose.yml +15 -0
- package/.dockerignore +37 -0
- package/.github/codeql-config.yml +4 -0
- package/.github/copilot-instructions.md +138 -0
- package/.github/prompts/memory.prompt.md +120 -0
- package/.github/workflows/auto-tag-release.yml +84 -0
- package/.github/workflows/codeql-analysis.yml +56 -0
- package/.github/workflows/npm-publish.yml +58 -0
- package/.github/workflows/pr-branch-validation.yml +78 -0
- package/.github/workflows/run-tests.yml +41 -0
- package/.github/workflows/sync-main-to-develop.yml +53 -0
- package/.handbook/GIT_GUIDELINES.md +250 -0
- package/.handbook/PACKAGE_VERSIONING.md +140 -0
- package/.handbook/STACK.md +75 -0
- package/.prettierrc +4 -0
- package/.vscode/extensions.json +44 -0
- package/.vscode/settings.json +40 -0
- package/CONTRIBUTING.md +261 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/dist/examples/async-import/app.module.d.ts +2 -0
- package/dist/examples/async-import/app.module.js +33 -0
- package/dist/examples/async-import/app.module.js.map +1 -0
- package/dist/examples/async-import/main.d.ts +1 -0
- package/dist/examples/async-import/main.js +17 -0
- package/dist/examples/async-import/main.js.map +1 -0
- package/dist/examples/guards/app.module.d.ts +6 -0
- package/dist/examples/guards/app.module.js +48 -0
- package/dist/examples/guards/app.module.js.map +1 -0
- package/dist/examples/guards/guards.resolver.d.ts +13 -0
- package/dist/examples/guards/guards.resolver.js +61 -0
- package/dist/examples/guards/guards.resolver.js.map +1 -0
- package/dist/examples/guards/main.d.ts +1 -0
- package/dist/examples/guards/main.js +11 -0
- package/dist/examples/guards/main.js.map +1 -0
- package/dist/examples/mixed/app.module.d.ts +2 -0
- package/dist/examples/mixed/app.module.js +31 -0
- package/dist/examples/mixed/app.module.js.map +1 -0
- package/dist/examples/mixed/main.d.ts +1 -0
- package/dist/examples/mixed/main.js +11 -0
- package/dist/examples/mixed/main.js.map +1 -0
- package/dist/examples/mixed/mixed.resolver.d.ts +6 -0
- package/dist/examples/mixed/mixed.resolver.js +78 -0
- package/dist/examples/mixed/mixed.resolver.js.map +1 -0
- package/dist/examples/prompts/app.module.d.ts +2 -0
- package/dist/examples/prompts/app.module.js +31 -0
- package/dist/examples/prompts/app.module.js.map +1 -0
- package/dist/examples/prompts/main.d.ts +1 -0
- package/dist/examples/prompts/main.js +11 -0
- package/dist/examples/prompts/main.js.map +1 -0
- package/dist/examples/prompts/prompts.resolver.d.ts +14 -0
- package/dist/examples/prompts/prompts.resolver.js +165 -0
- package/dist/examples/prompts/prompts.resolver.js.map +1 -0
- package/dist/examples/resources/app.module.d.ts +2 -0
- package/dist/examples/resources/app.module.js +31 -0
- package/dist/examples/resources/app.module.js.map +1 -0
- package/dist/examples/resources/main.d.ts +1 -0
- package/dist/examples/resources/main.js +11 -0
- package/dist/examples/resources/main.js.map +1 -0
- package/dist/examples/resources/resources.resolver.d.ts +12 -0
- package/dist/examples/resources/resources.resolver.js +114 -0
- package/dist/examples/resources/resources.resolver.js.map +1 -0
- package/dist/examples/tools/app.module.d.ts +2 -0
- package/dist/examples/tools/app.module.js +31 -0
- package/dist/examples/tools/app.module.js.map +1 -0
- package/dist/examples/tools/main.d.ts +1 -0
- package/dist/examples/tools/main.js +11 -0
- package/dist/examples/tools/main.js.map +1 -0
- package/dist/examples/tools/tools.resolver.d.ts +23 -0
- package/dist/examples/tools/tools.resolver.js +175 -0
- package/dist/examples/tools/tools.resolver.js.map +1 -0
- package/dist/src/controllers/sse/index.d.ts +2 -0
- package/dist/src/controllers/sse/index.js +19 -0
- package/dist/src/controllers/sse/index.js.map +1 -0
- package/dist/src/controllers/sse/sse.controller.d.ts +8 -0
- package/dist/src/controllers/sse/sse.controller.js +51 -0
- package/dist/src/controllers/sse/sse.controller.js.map +1 -0
- package/dist/src/controllers/sse/sse.service.d.ts +16 -0
- package/dist/src/controllers/sse/sse.service.js +78 -0
- package/dist/src/controllers/sse/sse.service.js.map +1 -0
- package/dist/src/controllers/streamable/index.d.ts +2 -0
- package/dist/src/controllers/streamable/index.js +19 -0
- package/dist/src/controllers/streamable/index.js.map +1 -0
- package/dist/src/controllers/streamable/streamable.controller.d.ts +9 -0
- package/dist/src/controllers/streamable/streamable.controller.js +62 -0
- package/dist/src/controllers/streamable/streamable.controller.js.map +1 -0
- package/dist/src/controllers/streamable/streamable.service.d.ts +24 -0
- package/dist/src/controllers/streamable/streamable.service.js +117 -0
- package/dist/src/controllers/streamable/streamable.service.js.map +1 -0
- package/dist/src/decorators/capabilities.constants.d.ts +4 -0
- package/dist/src/decorators/capabilities.constants.js +8 -0
- package/dist/src/decorators/capabilities.constants.js.map +1 -0
- package/dist/src/decorators/capabilities.decorators.d.ts +8 -0
- package/dist/src/decorators/capabilities.decorators.js +49 -0
- package/dist/src/decorators/capabilities.decorators.js.map +1 -0
- package/dist/src/decorators/index.d.ts +2 -0
- package/dist/src/decorators/index.js +19 -0
- package/dist/src/decorators/index.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces/capabilities.interface.d.ts +52 -0
- package/dist/src/interfaces/capabilities.interface.js +3 -0
- package/dist/src/interfaces/capabilities.interface.js.map +1 -0
- package/dist/src/interfaces/guards.interface.d.ts +4 -0
- package/dist/src/interfaces/guards.interface.js +3 -0
- package/dist/src/interfaces/guards.interface.js.map +1 -0
- package/dist/src/interfaces/index.d.ts +2 -0
- package/dist/src/interfaces/index.js +19 -0
- package/dist/src/interfaces/index.js.map +1 -0
- package/dist/src/interfaces/mcp-server-options.interface.d.ts +42 -0
- package/dist/src/interfaces/mcp-server-options.interface.js +3 -0
- package/dist/src/interfaces/mcp-server-options.interface.js.map +1 -0
- package/dist/src/mcp.module.d.ts +13 -0
- package/dist/src/mcp.module.js +176 -0
- package/dist/src/mcp.module.js.map +1 -0
- package/dist/src/registry/discovery.service.d.ts +16 -0
- package/dist/src/registry/discovery.service.js +85 -0
- package/dist/src/registry/discovery.service.js.map +1 -0
- package/dist/src/registry/index.d.ts +2 -0
- package/dist/src/registry/index.js +19 -0
- package/dist/src/registry/index.js.map +1 -0
- package/dist/src/registry/logger.service.d.ts +16 -0
- package/dist/src/registry/logger.service.js +97 -0
- package/dist/src/registry/logger.service.js.map +1 -0
- package/dist/src/registry/registry.service.d.ts +14 -0
- package/dist/src/registry/registry.service.js +165 -0
- package/dist/src/registry/registry.service.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/eslint.config.mjs +40 -0
- package/examples/README.md +56 -0
- package/examples/async-import/app.module.ts +22 -0
- package/examples/async-import/main.ts +15 -0
- package/examples/guards/app.module.ts +44 -0
- package/examples/guards/guards.resolver.ts +52 -0
- package/examples/guards/main.ts +11 -0
- package/examples/mixed/app.module.ts +20 -0
- package/examples/mixed/main.ts +11 -0
- package/examples/mixed/mixed.resolver.ts +56 -0
- package/examples/prompts/app.module.ts +20 -0
- package/examples/prompts/main.ts +11 -0
- package/examples/prompts/prompts.resolver.ts +184 -0
- package/examples/resources/app.module.ts +19 -0
- package/examples/resources/main.ts +11 -0
- package/examples/resources/resources.resolver.ts +123 -0
- package/examples/tools/app.module.ts +20 -0
- package/examples/tools/main.ts +11 -0
- package/examples/tools/tools.resolver.ts +205 -0
- package/nest-cli.json +8 -0
- package/package.json +106 -0
- package/scripts/npm-publish.js +301 -0
- package/src/controllers/sse/index.ts +2 -0
- package/src/controllers/sse/sse.controller.ts +19 -0
- package/src/controllers/sse/sse.service.ts +90 -0
- package/src/controllers/streamable/index.ts +2 -0
- package/src/controllers/streamable/streamable.controller.ts +24 -0
- package/src/controllers/streamable/streamable.service.ts +168 -0
- package/src/decorators/capabilities.constants.ts +7 -0
- package/src/decorators/capabilities.decorators.ts +150 -0
- package/src/decorators/index.ts +2 -0
- package/src/index.ts +11 -0
- package/src/interfaces/capabilities.interface.ts +95 -0
- package/src/interfaces/guards.interface.ts +13 -0
- package/src/interfaces/index.ts +2 -0
- package/src/interfaces/mcp-server-options.interface.ts +105 -0
- package/src/mcp.module.ts +233 -0
- package/src/mcp.service.spec.ts +28 -0
- package/src/registry/discovery.service.ts +116 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/logger.service.ts +143 -0
- package/src/registry/registry.service.ts +281 -0
- package/test/base.e2e-spec.ts +74 -0
- package/test/jest-e2e.json +9 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NestFactory } from '@nestjs/core';
|
|
2
|
+
|
|
3
|
+
import { AppModule } from './app.module';
|
|
4
|
+
|
|
5
|
+
async function bootstrap() {
|
|
6
|
+
const app = await NestFactory.create(AppModule);
|
|
7
|
+
await app.listen(3000);
|
|
8
|
+
console.log('Resources example server running on http://localhost:3000');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
void bootstrap();
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { CallToolResult } from '@modelcontextprotocol/sdk/types';
|
|
2
|
+
import { randomInt, randomUUID } from 'crypto';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { Resolver, Tool } from '../../src';
|
|
6
|
+
|
|
7
|
+
@Resolver('tools')
|
|
8
|
+
export class ToolsResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Simple tool with only a name
|
|
11
|
+
* Use case: Basic functionality that requires no parameters
|
|
12
|
+
*/
|
|
13
|
+
@Tool({
|
|
14
|
+
name: 'server_health_check',
|
|
15
|
+
})
|
|
16
|
+
healthCheck(): CallToolResult {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: 'text',
|
|
21
|
+
text: 'Server is operational. All systems running normally.',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tool with name and parameter schema
|
|
29
|
+
* Use case: When you need validated input parameters
|
|
30
|
+
*/
|
|
31
|
+
@Tool({
|
|
32
|
+
name: 'calculate_discount',
|
|
33
|
+
paramSchema: {
|
|
34
|
+
price: z.number().positive().min(0.01),
|
|
35
|
+
discountPercentage: z.number().min(0).max(100),
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
calculateDiscount({
|
|
39
|
+
price,
|
|
40
|
+
discountPercentage,
|
|
41
|
+
}: {
|
|
42
|
+
price: number;
|
|
43
|
+
discountPercentage: number;
|
|
44
|
+
}): CallToolResult {
|
|
45
|
+
const discountAmount = price * (discountPercentage / 100);
|
|
46
|
+
const finalPrice = price - discountAmount;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: `Original price: $${price.toFixed(2)}\nDiscount: $${discountAmount.toFixed(2)} (${discountPercentage}%)\nFinal price: $${finalPrice.toFixed(2)}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tool with name and description
|
|
60
|
+
* Use case: When additional context helps the user understand the tool's purpose
|
|
61
|
+
*/
|
|
62
|
+
@Tool({
|
|
63
|
+
name: 'generate_unique_id',
|
|
64
|
+
description:
|
|
65
|
+
'Generates a cryptographically strong unique identifier suitable for database keys or session tokens',
|
|
66
|
+
})
|
|
67
|
+
generateUniqueId(): CallToolResult {
|
|
68
|
+
// Simple UUID v4 implementation for example purposes
|
|
69
|
+
const uuid = randomUUID();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: `Generated ID: ${uuid}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Comprehensive tool with all options
|
|
83
|
+
* Use case: Complex functionality requiring validated input and clear explanation
|
|
84
|
+
*/
|
|
85
|
+
@Tool({
|
|
86
|
+
name: 'weather_forecast',
|
|
87
|
+
description:
|
|
88
|
+
'Retrieves weather forecast for a specified location and timeframe',
|
|
89
|
+
paramSchema: {
|
|
90
|
+
location: z.string().min(2).max(100),
|
|
91
|
+
days: z.number().int().min(1).max(7).default(3),
|
|
92
|
+
tempUnit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
getWeatherForecast({
|
|
96
|
+
location,
|
|
97
|
+
days,
|
|
98
|
+
tempUnit,
|
|
99
|
+
}: {
|
|
100
|
+
location: string;
|
|
101
|
+
days: number;
|
|
102
|
+
tempUnit: 'celsius' | 'fahrenheit';
|
|
103
|
+
}): CallToolResult {
|
|
104
|
+
// Mock weather data for demonstration
|
|
105
|
+
const weatherTypes = [
|
|
106
|
+
'Sunny',
|
|
107
|
+
'Partly Cloudy',
|
|
108
|
+
'Cloudy',
|
|
109
|
+
'Rainy',
|
|
110
|
+
'Thunderstorms',
|
|
111
|
+
];
|
|
112
|
+
const forecast = Array.from({ length: days }, (_, i) => {
|
|
113
|
+
const date = new Date();
|
|
114
|
+
date.setDate(date.getDate() + i);
|
|
115
|
+
|
|
116
|
+
// Generate mock temperature (60-85°F range or 15-30°C range)
|
|
117
|
+
let temp = Math.floor(Math.random() * 15) + 15; // celsius
|
|
118
|
+
if (tempUnit === 'fahrenheit') {
|
|
119
|
+
temp = Math.round((temp * 9) / 5 + 32);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
date: date.toDateString(),
|
|
124
|
+
condition:
|
|
125
|
+
weatherTypes[Math.floor(Math.random() * weatherTypes.length)],
|
|
126
|
+
temperature: temp,
|
|
127
|
+
unit: tempUnit === 'celsius' ? '°C' : '°F',
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const formattedForecast = forecast
|
|
132
|
+
.map(
|
|
133
|
+
(day) => `${day.date}: ${day.condition}, ${day.temperature}${day.unit}`,
|
|
134
|
+
)
|
|
135
|
+
.join('\n');
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: `Weather forecast for ${location} (next ${days} days):\n\n${formattedForecast}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Tool with complex schema validation
|
|
149
|
+
* Use case: When you need sophisticated input validation
|
|
150
|
+
*/
|
|
151
|
+
@Tool({
|
|
152
|
+
name: 'register_user',
|
|
153
|
+
description: 'Registers a new user in the system with validation',
|
|
154
|
+
paramSchema: {
|
|
155
|
+
username: z
|
|
156
|
+
.string()
|
|
157
|
+
.min(3)
|
|
158
|
+
.max(20)
|
|
159
|
+
.regex(/^[a-zA-Z0-9_]+$/),
|
|
160
|
+
email: z.string().email(),
|
|
161
|
+
age: z.number().int().min(18).optional(),
|
|
162
|
+
preferences: z
|
|
163
|
+
.object({
|
|
164
|
+
theme: z.enum(['light', 'dark', 'system']).default('system'),
|
|
165
|
+
notifications: z.boolean().default(true),
|
|
166
|
+
})
|
|
167
|
+
.optional(),
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
registerUser({
|
|
171
|
+
username,
|
|
172
|
+
email,
|
|
173
|
+
age,
|
|
174
|
+
preferences,
|
|
175
|
+
}: {
|
|
176
|
+
username: string;
|
|
177
|
+
email: string;
|
|
178
|
+
age?: number;
|
|
179
|
+
preferences?: {
|
|
180
|
+
theme: 'light' | 'dark' | 'system';
|
|
181
|
+
notifications: boolean;
|
|
182
|
+
};
|
|
183
|
+
}): CallToolResult {
|
|
184
|
+
const userId = randomInt(10000, 100000);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: 'text',
|
|
190
|
+
text: `User registered successfully!\n
|
|
191
|
+
User ID: ${userId}
|
|
192
|
+
Username: ${username}
|
|
193
|
+
Email: ${email}
|
|
194
|
+
${age ? `Age: ${age}` : ''}
|
|
195
|
+
${
|
|
196
|
+
preferences
|
|
197
|
+
? `Theme: ${preferences.theme}
|
|
198
|
+
Notifications: ${preferences.notifications ? 'Enabled' : 'Disabled'}`
|
|
199
|
+
: ''
|
|
200
|
+
}`,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
package/nest-cli.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nestjs-mcp/server",
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
|
+
"description": "Modular library for building scalable MCP servers with NestJS, providing decorators and integration patterns as a wrapper for the official MCP TypeScript SDK.",
|
|
5
|
+
"author": "Adrián Darío Hidalgo Flores",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22",
|
|
9
|
+
"pnpm": ">=10"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/adrian-d-hidalgo/nestjs-mcp-server.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/adrian-d-hidalgo/nestjs-mcp-server#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/adrian-d-hidalgo/nestjs-mcp-server/issues"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"decorators",
|
|
26
|
+
"integration",
|
|
27
|
+
"large-language-models",
|
|
28
|
+
"llm",
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"module",
|
|
32
|
+
"nestjs",
|
|
33
|
+
"sdk",
|
|
34
|
+
"server",
|
|
35
|
+
"typescript"
|
|
36
|
+
],
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@nestjs/common": "^11.0.1",
|
|
39
|
+
"@nestjs/core": "^11.0.1",
|
|
40
|
+
"@nestjs/platform-express": "^11.0.1",
|
|
41
|
+
"reflect-metadata": "^0.2.2",
|
|
42
|
+
"rxjs": "^7.8.1"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
46
|
+
"zod": "^3.24.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
50
|
+
"@eslint/js": "^9.18.0",
|
|
51
|
+
"@nestjs/cli": "^11.0.0",
|
|
52
|
+
"@nestjs/config": "^4.0.2",
|
|
53
|
+
"@nestjs/schematics": "^11.0.0",
|
|
54
|
+
"@nestjs/testing": "^11.0.1",
|
|
55
|
+
"@swc/cli": "^0.6.0",
|
|
56
|
+
"@swc/core": "^1.10.7",
|
|
57
|
+
"@types/express": "^5.0.0",
|
|
58
|
+
"@types/jest": "^29.5.14",
|
|
59
|
+
"@types/node": "^22.10.7",
|
|
60
|
+
"@types/supertest": "^6.0.2",
|
|
61
|
+
"eslint": "^9.18.0",
|
|
62
|
+
"eslint-config-prettier": "^10.0.1",
|
|
63
|
+
"eslint-plugin-prettier": "^5.2.2",
|
|
64
|
+
"globals": "^16.0.0",
|
|
65
|
+
"jest": "^29.7.0",
|
|
66
|
+
"prettier": "^3.4.2",
|
|
67
|
+
"source-map-support": "^0.5.21",
|
|
68
|
+
"supertest": "^7.0.0",
|
|
69
|
+
"ts-jest": "^29.2.5",
|
|
70
|
+
"ts-loader": "^9.5.2",
|
|
71
|
+
"ts-node": "^10.9.2",
|
|
72
|
+
"tsconfig-paths": "^4.2.0",
|
|
73
|
+
"typescript": "^5.7.3",
|
|
74
|
+
"typescript-eslint": "^8.20.0"
|
|
75
|
+
},
|
|
76
|
+
"jest": {
|
|
77
|
+
"moduleFileExtensions": [
|
|
78
|
+
"js",
|
|
79
|
+
"json",
|
|
80
|
+
"ts"
|
|
81
|
+
],
|
|
82
|
+
"rootDir": "src",
|
|
83
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
84
|
+
"transform": {
|
|
85
|
+
"^.+\\.(t|j)s$": "ts-jest"
|
|
86
|
+
},
|
|
87
|
+
"collectCoverageFrom": [
|
|
88
|
+
"**/*.(t|j)s"
|
|
89
|
+
],
|
|
90
|
+
"coverageDirectory": "../coverage",
|
|
91
|
+
"testEnvironment": "node"
|
|
92
|
+
},
|
|
93
|
+
"scripts": {
|
|
94
|
+
"build": "nest build",
|
|
95
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
96
|
+
"start:example": "npx -y ts-node-dev --respawn examples/$EXAMPLE/main.ts",
|
|
97
|
+
"start:inspector": "npx -y @modelcontextprotocol/inspector",
|
|
98
|
+
"lint": "eslint \"{src,test,examples}/**/*.ts\" --fix",
|
|
99
|
+
"typecheck": "tsc --noEmit",
|
|
100
|
+
"test": "jest",
|
|
101
|
+
"test:watch": "jest --watch",
|
|
102
|
+
"test:cov": "jest --coverage",
|
|
103
|
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
104
|
+
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* npm-publish.js
|
|
5
|
+
*
|
|
6
|
+
* Smart publish script for MCP Server packages.
|
|
7
|
+
* - Detects branch and version
|
|
8
|
+
* - Enforces branch and versioning rules
|
|
9
|
+
* - Decides release type (alpha, beta, rc, release)
|
|
10
|
+
* - Runs build and appropriate publish command
|
|
11
|
+
* - Prints clear errors and exits non-zero on rule violation
|
|
12
|
+
*
|
|
13
|
+
* Usage: pnpm run publish
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// --- CONFIGURABLE RULES ---
|
|
21
|
+
const ALLOWED_BRANCHES = [/^release\//, /^fix\//];
|
|
22
|
+
const RELEASE_BRANCH = /^release\//;
|
|
23
|
+
const VERSION_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$/;
|
|
24
|
+
|
|
25
|
+
function getCurrentBranch() {
|
|
26
|
+
try {
|
|
27
|
+
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('Error: Not a git repository or git not installed.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getPackageJson() {
|
|
35
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
36
|
+
if (!fs.existsSync(pkgPath)) {
|
|
37
|
+
console.error('Error: package.json not found.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getReleaseType(version) {
|
|
44
|
+
const match = VERSION_REGEX.exec(version);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
return match[4] || 'release';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function checkAllowedBranch(branch) {
|
|
50
|
+
return ALLOWED_BRANCHES.some((re) => re.test(branch));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checkReleaseBranch(branch) {
|
|
54
|
+
return RELEASE_BRANCH.test(branch);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function run(cmd, opts = {}) {
|
|
58
|
+
try {
|
|
59
|
+
execSync(cmd, { stdio: 'inherit', ...opts });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
process.exit(e.status || 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkNpmAuth() {
|
|
66
|
+
try {
|
|
67
|
+
execSync('npm whoami', { stdio: 'ignore' });
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error('Error: You are not authenticated with npm. Run `npm login` and try again.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkBuildSuccess() {
|
|
75
|
+
const distPath = path.resolve(process.cwd(), 'dist');
|
|
76
|
+
if (!fs.existsSync(distPath) || fs.readdirSync(distPath).length === 0) {
|
|
77
|
+
console.error('Error: Build failed or dist/ directory is empty. Aborting publish.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function checkNpmVersion(version) {
|
|
83
|
+
try {
|
|
84
|
+
const pkg = getPackageJson();
|
|
85
|
+
const name = pkg.name;
|
|
86
|
+
// Get all versions from npm
|
|
87
|
+
const result = execSync(`npm view ${name} versions --json`).toString();
|
|
88
|
+
const versions = JSON.parse(result);
|
|
89
|
+
if (versions.includes(version)) {
|
|
90
|
+
console.error(`Error: Version ${version} already exists on npm. Bump the version before publishing.`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// If the package is not published yet, ignore
|
|
95
|
+
if (e.stderr && e.stderr.toString().includes('E404')) return;
|
|
96
|
+
console.error('Error checking npm registry:', e.message || e);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getTagVersion() {
|
|
102
|
+
// Try to get tag from environment (e.g., GITHUB_REF) or from git
|
|
103
|
+
const envTag = process.env.GITHUB_REF;
|
|
104
|
+
if (envTag && envTag.startsWith('refs/tags/v')) {
|
|
105
|
+
return envTag.replace('refs/tags/v', '');
|
|
106
|
+
}
|
|
107
|
+
// Fallback: try to get latest tag from git
|
|
108
|
+
try {
|
|
109
|
+
const tag = execSync('git describe --tags --abbrev=0').toString().trim();
|
|
110
|
+
return tag.startsWith('v') ? tag.slice(1) : tag;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getSemverMain(version) {
|
|
117
|
+
// Extracts major.minor.patch
|
|
118
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
119
|
+
return match ? match[0] : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateTagStructure(tagVersion) {
|
|
123
|
+
// Accepts: 1.2.3, 1.2.3-alpha.0, 1.2.3-beta.1, 1.2.3-rc.2
|
|
124
|
+
const valid = /^\d+\.\d+\.\d+(-((alpha|beta|rc)\.(\d+)))?$/.test(tagVersion);
|
|
125
|
+
if (!valid) {
|
|
126
|
+
console.error(`Error: Tag version '${tagVersion}' does not match the required pattern: v<semver>[-<prerelease>.<number>]`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isProductiveTag(tagVersion) {
|
|
132
|
+
// Returns true if tag is X.Y.Z (no pre-release)
|
|
133
|
+
return /^\d+\.\d+\.\d+$/.test(tagVersion);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getNpmTagFromVersion(version) {
|
|
137
|
+
// Extracts prerelease part (e.g., alpha.0, beta.1, rc.2) or returns 'latest' for final
|
|
138
|
+
const match = version.match(/^\d+\.\d+\.\d+-(alpha|beta|rc)\.(\d+)$/);
|
|
139
|
+
if (match) {
|
|
140
|
+
return `${match[1]}.${match[2]}`;
|
|
141
|
+
}
|
|
142
|
+
return 'latest';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getNextPrereleaseTag(baseVersion, type) {
|
|
146
|
+
// Find all tags for this version and type, return next incremental tag
|
|
147
|
+
const tagPrefix = `v${baseVersion}-${type}.`;
|
|
148
|
+
let maxNum = -1;
|
|
149
|
+
try {
|
|
150
|
+
const allTags = execSync('git tag', { encoding: 'utf8' })
|
|
151
|
+
.split('\n')
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
allTags.forEach(tag => {
|
|
154
|
+
if (tag.startsWith(tagPrefix)) {
|
|
155
|
+
const match = tag.match(new RegExp(`^v${baseVersion}-${type}\.(\\d+)$`));
|
|
156
|
+
if (match) {
|
|
157
|
+
const num = parseInt(match[1], 10);
|
|
158
|
+
if (num > maxNum) maxNum = num;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
return `v${baseVersion}-${type}.${maxNum + 1}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateBaseTagFormat(tag) {
|
|
169
|
+
// Must be vX.Y.Z
|
|
170
|
+
if (!/^v\d+\.\d+\.\d+$/.test(tag)) {
|
|
171
|
+
console.error('Error: Tag must be in the format vX.Y.Z (e.g., v0.1.0)');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function checkNpmTagExists(pkgName, version) {
|
|
177
|
+
try {
|
|
178
|
+
const result = execSync(`npm view ${pkgName} versions --json`).toString();
|
|
179
|
+
const versions = JSON.parse(result);
|
|
180
|
+
if (versions.includes(version)) {
|
|
181
|
+
console.error(`Error: Version ${version} already exists on npm.`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
// If the package is not published yet, ignore
|
|
186
|
+
if (e.stderr && e.stderr.toString().includes('E404')) return;
|
|
187
|
+
console.error('Error checking npm registry:', e.message || e);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (process.argv[2] === 'next-tag') {
|
|
193
|
+
// Usage: node scripts/npm-publish.js next-tag v0.1.0 beta
|
|
194
|
+
const baseTag = process.argv[3];
|
|
195
|
+
const type = process.argv[4];
|
|
196
|
+
validateBaseTagFormat(baseTag);
|
|
197
|
+
const baseVersion = baseTag.slice(1); // remove 'v'
|
|
198
|
+
if (!type || !['alpha', 'beta', 'rc'].includes(type)) {
|
|
199
|
+
console.error('Usage: node scripts/npm-publish.js next-tag vX.Y.Z <alpha|beta|rc>');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
if (process.env.CI === 'true') {
|
|
203
|
+
// In CI, just return the base version and check npm
|
|
204
|
+
const pkg = getPackageJson();
|
|
205
|
+
checkNpmTagExists(pkg.name, baseVersion);
|
|
206
|
+
console.log(baseVersion);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
// Local: suggest next pre-release tag
|
|
210
|
+
const nextTag = getNextPrereleaseTag(baseVersion, type);
|
|
211
|
+
console.log(nextTag);
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function main() {
|
|
216
|
+
if (process.argv[2] === 'next-tag') {
|
|
217
|
+
// Usage: node scripts/npm-publish.js next-tag 0.1.0 alpha
|
|
218
|
+
const baseVersion = process.argv[3];
|
|
219
|
+
const type = process.argv[4];
|
|
220
|
+
if (!baseVersion || !type || !['alpha', 'beta', 'rc'].includes(type)) {
|
|
221
|
+
console.error('Usage: node scripts/npm-publish.js next-tag <baseVersion> <alpha|beta|rc>');
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
const nextTag = getNextPrereleaseTag(baseVersion, type);
|
|
225
|
+
console.log(nextTag);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const branch = getCurrentBranch();
|
|
230
|
+
const pkg = getPackageJson();
|
|
231
|
+
const version = pkg.version;
|
|
232
|
+
const releaseType = getReleaseType(version);
|
|
233
|
+
|
|
234
|
+
// --- Tag version validation (if running from a tag) ---
|
|
235
|
+
const tagVersion = getTagVersion();
|
|
236
|
+
if (tagVersion) {
|
|
237
|
+
validateTagStructure(tagVersion);
|
|
238
|
+
const tagSemver = getSemverMain(tagVersion);
|
|
239
|
+
const pkgSemver = getSemverMain(version);
|
|
240
|
+
if (tagSemver !== pkgSemver) {
|
|
241
|
+
console.error(`Error: Tag semver (${tagSemver}) does not match package.json semver (${pkgSemver}).`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Default to dry-run unless --no-dry-run is passed or CI is true
|
|
247
|
+
const isDryRun = !process.argv.includes('--no-dry-run') && process.env.CI !== 'true';
|
|
248
|
+
|
|
249
|
+
// --- Branch validation ---
|
|
250
|
+
if (!checkAllowedBranch(branch)) {
|
|
251
|
+
console.error(`\nError: Publishing is only allowed from release/* or fix/* branches. Current: ${branch}`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- NPM authentication check ---
|
|
256
|
+
checkNpmAuth();
|
|
257
|
+
|
|
258
|
+
// --- Build before publish ---
|
|
259
|
+
run('pnpm run build');
|
|
260
|
+
checkBuildSuccess();
|
|
261
|
+
|
|
262
|
+
// --- NPM version check ---
|
|
263
|
+
checkNpmVersion(version);
|
|
264
|
+
|
|
265
|
+
// --- Restrict final releases to CI/CD only ---
|
|
266
|
+
if (releaseType === 'release' && process.env.CI !== 'true') {
|
|
267
|
+
console.error('Error: Final releases can only be published from CI/CD pipelines. Set CI=true in your environment.');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Decide publish command ---
|
|
272
|
+
let publishCmd = 'npm publish';
|
|
273
|
+
if (isDryRun) {
|
|
274
|
+
publishCmd += ' --dry-run --no-git-checks';
|
|
275
|
+
}
|
|
276
|
+
const npmTag = getNpmTagFromVersion(version);
|
|
277
|
+
if (npmTag !== 'latest') {
|
|
278
|
+
publishCmd += ` --tag ${npmTag}`;
|
|
279
|
+
// For alpha/beta/rc, set access (alpha is restricted, others are public)
|
|
280
|
+
if (npmTag.startsWith('alpha')) {
|
|
281
|
+
publishCmd += ' --access=restricted';
|
|
282
|
+
} else {
|
|
283
|
+
publishCmd += ' --access=public';
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
if (!checkReleaseBranch(branch)) {
|
|
287
|
+
console.error('Error: Final releases can only be published from release/* branches.');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
publishCmd += ' --access=public';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- Publish ---
|
|
294
|
+
console.log(`\nPublishing version ${version} as ${npmTag} from branch ${branch}...\n`);
|
|
295
|
+
if (isDryRun) {
|
|
296
|
+
console.log('Dry run enabled by default: No package will actually be published. Use --no-dry-run or set CI=true to publish for real.');
|
|
297
|
+
}
|
|
298
|
+
run(publishCmd);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
main();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Controller, Get, Post, Req, Res } from '@nestjs/common';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { SseService } from './sse.service';
|
|
5
|
+
|
|
6
|
+
@Controller()
|
|
7
|
+
export class SseController {
|
|
8
|
+
constructor(private readonly service: SseService) {}
|
|
9
|
+
|
|
10
|
+
@Get('sse')
|
|
11
|
+
async handleSse(@Req() req: Request, @Res() res: Response) {
|
|
12
|
+
await this.service.handleSse(req, res);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Post('messages')
|
|
16
|
+
async handleMessages(@Req() req: Request, @Res() res: Response) {
|
|
17
|
+
await this.service.handleMessage(req, res);
|
|
18
|
+
}
|
|
19
|
+
}
|