@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.
Files changed (178) hide show
  1. package/.copilotignore +38 -0
  2. package/.devcontainer/Dockerfile.dev +28 -0
  3. package/.devcontainer/devcontainer.json +56 -0
  4. package/.devcontainer/docker-compose.yml +15 -0
  5. package/.dockerignore +37 -0
  6. package/.github/codeql-config.yml +4 -0
  7. package/.github/copilot-instructions.md +138 -0
  8. package/.github/prompts/memory.prompt.md +120 -0
  9. package/.github/workflows/auto-tag-release.yml +84 -0
  10. package/.github/workflows/codeql-analysis.yml +56 -0
  11. package/.github/workflows/npm-publish.yml +58 -0
  12. package/.github/workflows/pr-branch-validation.yml +78 -0
  13. package/.github/workflows/run-tests.yml +41 -0
  14. package/.github/workflows/sync-main-to-develop.yml +53 -0
  15. package/.handbook/GIT_GUIDELINES.md +250 -0
  16. package/.handbook/PACKAGE_VERSIONING.md +140 -0
  17. package/.handbook/STACK.md +75 -0
  18. package/.prettierrc +4 -0
  19. package/.vscode/extensions.json +44 -0
  20. package/.vscode/settings.json +40 -0
  21. package/CONTRIBUTING.md +261 -0
  22. package/LICENSE +21 -0
  23. package/README.md +490 -0
  24. package/dist/examples/async-import/app.module.d.ts +2 -0
  25. package/dist/examples/async-import/app.module.js +33 -0
  26. package/dist/examples/async-import/app.module.js.map +1 -0
  27. package/dist/examples/async-import/main.d.ts +1 -0
  28. package/dist/examples/async-import/main.js +17 -0
  29. package/dist/examples/async-import/main.js.map +1 -0
  30. package/dist/examples/guards/app.module.d.ts +6 -0
  31. package/dist/examples/guards/app.module.js +48 -0
  32. package/dist/examples/guards/app.module.js.map +1 -0
  33. package/dist/examples/guards/guards.resolver.d.ts +13 -0
  34. package/dist/examples/guards/guards.resolver.js +61 -0
  35. package/dist/examples/guards/guards.resolver.js.map +1 -0
  36. package/dist/examples/guards/main.d.ts +1 -0
  37. package/dist/examples/guards/main.js +11 -0
  38. package/dist/examples/guards/main.js.map +1 -0
  39. package/dist/examples/mixed/app.module.d.ts +2 -0
  40. package/dist/examples/mixed/app.module.js +31 -0
  41. package/dist/examples/mixed/app.module.js.map +1 -0
  42. package/dist/examples/mixed/main.d.ts +1 -0
  43. package/dist/examples/mixed/main.js +11 -0
  44. package/dist/examples/mixed/main.js.map +1 -0
  45. package/dist/examples/mixed/mixed.resolver.d.ts +6 -0
  46. package/dist/examples/mixed/mixed.resolver.js +78 -0
  47. package/dist/examples/mixed/mixed.resolver.js.map +1 -0
  48. package/dist/examples/prompts/app.module.d.ts +2 -0
  49. package/dist/examples/prompts/app.module.js +31 -0
  50. package/dist/examples/prompts/app.module.js.map +1 -0
  51. package/dist/examples/prompts/main.d.ts +1 -0
  52. package/dist/examples/prompts/main.js +11 -0
  53. package/dist/examples/prompts/main.js.map +1 -0
  54. package/dist/examples/prompts/prompts.resolver.d.ts +14 -0
  55. package/dist/examples/prompts/prompts.resolver.js +165 -0
  56. package/dist/examples/prompts/prompts.resolver.js.map +1 -0
  57. package/dist/examples/resources/app.module.d.ts +2 -0
  58. package/dist/examples/resources/app.module.js +31 -0
  59. package/dist/examples/resources/app.module.js.map +1 -0
  60. package/dist/examples/resources/main.d.ts +1 -0
  61. package/dist/examples/resources/main.js +11 -0
  62. package/dist/examples/resources/main.js.map +1 -0
  63. package/dist/examples/resources/resources.resolver.d.ts +12 -0
  64. package/dist/examples/resources/resources.resolver.js +114 -0
  65. package/dist/examples/resources/resources.resolver.js.map +1 -0
  66. package/dist/examples/tools/app.module.d.ts +2 -0
  67. package/dist/examples/tools/app.module.js +31 -0
  68. package/dist/examples/tools/app.module.js.map +1 -0
  69. package/dist/examples/tools/main.d.ts +1 -0
  70. package/dist/examples/tools/main.js +11 -0
  71. package/dist/examples/tools/main.js.map +1 -0
  72. package/dist/examples/tools/tools.resolver.d.ts +23 -0
  73. package/dist/examples/tools/tools.resolver.js +175 -0
  74. package/dist/examples/tools/tools.resolver.js.map +1 -0
  75. package/dist/src/controllers/sse/index.d.ts +2 -0
  76. package/dist/src/controllers/sse/index.js +19 -0
  77. package/dist/src/controllers/sse/index.js.map +1 -0
  78. package/dist/src/controllers/sse/sse.controller.d.ts +8 -0
  79. package/dist/src/controllers/sse/sse.controller.js +51 -0
  80. package/dist/src/controllers/sse/sse.controller.js.map +1 -0
  81. package/dist/src/controllers/sse/sse.service.d.ts +16 -0
  82. package/dist/src/controllers/sse/sse.service.js +78 -0
  83. package/dist/src/controllers/sse/sse.service.js.map +1 -0
  84. package/dist/src/controllers/streamable/index.d.ts +2 -0
  85. package/dist/src/controllers/streamable/index.js +19 -0
  86. package/dist/src/controllers/streamable/index.js.map +1 -0
  87. package/dist/src/controllers/streamable/streamable.controller.d.ts +9 -0
  88. package/dist/src/controllers/streamable/streamable.controller.js +62 -0
  89. package/dist/src/controllers/streamable/streamable.controller.js.map +1 -0
  90. package/dist/src/controllers/streamable/streamable.service.d.ts +24 -0
  91. package/dist/src/controllers/streamable/streamable.service.js +117 -0
  92. package/dist/src/controllers/streamable/streamable.service.js.map +1 -0
  93. package/dist/src/decorators/capabilities.constants.d.ts +4 -0
  94. package/dist/src/decorators/capabilities.constants.js +8 -0
  95. package/dist/src/decorators/capabilities.constants.js.map +1 -0
  96. package/dist/src/decorators/capabilities.decorators.d.ts +8 -0
  97. package/dist/src/decorators/capabilities.decorators.js +49 -0
  98. package/dist/src/decorators/capabilities.decorators.js.map +1 -0
  99. package/dist/src/decorators/index.d.ts +2 -0
  100. package/dist/src/decorators/index.js +19 -0
  101. package/dist/src/decorators/index.js.map +1 -0
  102. package/dist/src/index.d.ts +4 -0
  103. package/dist/src/index.js +21 -0
  104. package/dist/src/index.js.map +1 -0
  105. package/dist/src/interfaces/capabilities.interface.d.ts +52 -0
  106. package/dist/src/interfaces/capabilities.interface.js +3 -0
  107. package/dist/src/interfaces/capabilities.interface.js.map +1 -0
  108. package/dist/src/interfaces/guards.interface.d.ts +4 -0
  109. package/dist/src/interfaces/guards.interface.js +3 -0
  110. package/dist/src/interfaces/guards.interface.js.map +1 -0
  111. package/dist/src/interfaces/index.d.ts +2 -0
  112. package/dist/src/interfaces/index.js +19 -0
  113. package/dist/src/interfaces/index.js.map +1 -0
  114. package/dist/src/interfaces/mcp-server-options.interface.d.ts +42 -0
  115. package/dist/src/interfaces/mcp-server-options.interface.js +3 -0
  116. package/dist/src/interfaces/mcp-server-options.interface.js.map +1 -0
  117. package/dist/src/mcp.module.d.ts +13 -0
  118. package/dist/src/mcp.module.js +176 -0
  119. package/dist/src/mcp.module.js.map +1 -0
  120. package/dist/src/registry/discovery.service.d.ts +16 -0
  121. package/dist/src/registry/discovery.service.js +85 -0
  122. package/dist/src/registry/discovery.service.js.map +1 -0
  123. package/dist/src/registry/index.d.ts +2 -0
  124. package/dist/src/registry/index.js +19 -0
  125. package/dist/src/registry/index.js.map +1 -0
  126. package/dist/src/registry/logger.service.d.ts +16 -0
  127. package/dist/src/registry/logger.service.js +97 -0
  128. package/dist/src/registry/logger.service.js.map +1 -0
  129. package/dist/src/registry/registry.service.d.ts +14 -0
  130. package/dist/src/registry/registry.service.js +165 -0
  131. package/dist/src/registry/registry.service.js.map +1 -0
  132. package/dist/tsconfig.build.tsbuildinfo +1 -0
  133. package/eslint.config.mjs +40 -0
  134. package/examples/README.md +56 -0
  135. package/examples/async-import/app.module.ts +22 -0
  136. package/examples/async-import/main.ts +15 -0
  137. package/examples/guards/app.module.ts +44 -0
  138. package/examples/guards/guards.resolver.ts +52 -0
  139. package/examples/guards/main.ts +11 -0
  140. package/examples/mixed/app.module.ts +20 -0
  141. package/examples/mixed/main.ts +11 -0
  142. package/examples/mixed/mixed.resolver.ts +56 -0
  143. package/examples/prompts/app.module.ts +20 -0
  144. package/examples/prompts/main.ts +11 -0
  145. package/examples/prompts/prompts.resolver.ts +184 -0
  146. package/examples/resources/app.module.ts +19 -0
  147. package/examples/resources/main.ts +11 -0
  148. package/examples/resources/resources.resolver.ts +123 -0
  149. package/examples/tools/app.module.ts +20 -0
  150. package/examples/tools/main.ts +11 -0
  151. package/examples/tools/tools.resolver.ts +205 -0
  152. package/nest-cli.json +8 -0
  153. package/package.json +106 -0
  154. package/scripts/npm-publish.js +301 -0
  155. package/src/controllers/sse/index.ts +2 -0
  156. package/src/controllers/sse/sse.controller.ts +19 -0
  157. package/src/controllers/sse/sse.service.ts +90 -0
  158. package/src/controllers/streamable/index.ts +2 -0
  159. package/src/controllers/streamable/streamable.controller.ts +24 -0
  160. package/src/controllers/streamable/streamable.service.ts +168 -0
  161. package/src/decorators/capabilities.constants.ts +7 -0
  162. package/src/decorators/capabilities.decorators.ts +150 -0
  163. package/src/decorators/index.ts +2 -0
  164. package/src/index.ts +11 -0
  165. package/src/interfaces/capabilities.interface.ts +95 -0
  166. package/src/interfaces/guards.interface.ts +13 -0
  167. package/src/interfaces/index.ts +2 -0
  168. package/src/interfaces/mcp-server-options.interface.ts +105 -0
  169. package/src/mcp.module.ts +233 -0
  170. package/src/mcp.service.spec.ts +28 -0
  171. package/src/registry/discovery.service.ts +116 -0
  172. package/src/registry/index.ts +2 -0
  173. package/src/registry/logger.service.ts +143 -0
  174. package/src/registry/registry.service.ts +281 -0
  175. package/test/base.e2e-spec.ts +74 -0
  176. package/test/jest-e2e.json +9 -0
  177. package/tsconfig.build.json +4 -0
  178. 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
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
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,2 @@
1
+ export * from './sse.controller';
2
+ export * from './sse.service';
@@ -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
+ }