@hed-hog/core 0.0.328 → 0.0.329

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 (150) hide show
  1. package/dist/ai/ai.service.d.ts +1 -1
  2. package/dist/auth/auth.controller.d.ts +2 -2
  3. package/dist/auth/auth.service.d.ts +2 -2
  4. package/dist/core/system.controller.d.ts +1 -1
  5. package/dist/core/system.service.d.ts +1 -1
  6. package/dist/core.module.d.ts.map +1 -1
  7. package/dist/core.module.js +4 -0
  8. package/dist/core.module.js.map +1 -1
  9. package/dist/dashboard/dashboard/dashboard.controller.d.ts +21 -21
  10. package/dist/dashboard/dashboard/dashboard.service.d.ts +21 -21
  11. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +11 -11
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +11 -11
  13. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +5 -5
  14. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +5 -5
  15. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +4 -4
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +4 -4
  17. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +4 -4
  18. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +4 -4
  19. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +3 -3
  20. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +3 -3
  21. package/dist/file/file.controller.d.ts +6 -6
  22. package/dist/file/file.service.d.ts +12 -12
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/mail/mail.controller.d.ts +2 -2
  28. package/dist/mail/mail.service.d.ts +2 -2
  29. package/dist/mail-sent/mail-sent.controller.d.ts +3 -3
  30. package/dist/mail-sent/mail-sent.service.d.ts +3 -3
  31. package/dist/mcp/decorators/mcp-tool.decorator.d.ts +4 -0
  32. package/dist/mcp/decorators/mcp-tool.decorator.d.ts.map +1 -0
  33. package/dist/mcp/decorators/mcp-tool.decorator.js +8 -0
  34. package/dist/mcp/decorators/mcp-tool.decorator.js.map +1 -0
  35. package/dist/mcp/guards/mcp-auth.guard.d.ts +9 -0
  36. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -0
  37. package/dist/mcp/guards/mcp-auth.guard.js +41 -0
  38. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -0
  39. package/dist/mcp/guards/mcp-permission.guard.d.ts +8 -0
  40. package/dist/mcp/guards/mcp-permission.guard.d.ts.map +1 -0
  41. package/dist/mcp/guards/mcp-permission.guard.js +57 -0
  42. package/dist/mcp/guards/mcp-permission.guard.js.map +1 -0
  43. package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts +7 -0
  44. package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts.map +1 -0
  45. package/dist/mcp/interfaces/mcp-tool-definition.interface.js +3 -0
  46. package/dist/mcp/interfaces/mcp-tool-definition.interface.js.map +1 -0
  47. package/dist/mcp/mcp-audit.service.d.ts +17 -0
  48. package/dist/mcp/mcp-audit.service.d.ts.map +1 -0
  49. package/dist/mcp/mcp-audit.service.js +46 -0
  50. package/dist/mcp/mcp-audit.service.js.map +1 -0
  51. package/dist/mcp/mcp-registry.service.d.ts +21 -0
  52. package/dist/mcp/mcp-registry.service.d.ts.map +1 -0
  53. package/dist/mcp/mcp-registry.service.js +69 -0
  54. package/dist/mcp/mcp-registry.service.js.map +1 -0
  55. package/dist/mcp/mcp-server.service.d.ts +12 -0
  56. package/dist/mcp/mcp-server.service.d.ts.map +1 -0
  57. package/dist/mcp/mcp-server.service.js +94 -0
  58. package/dist/mcp/mcp-server.service.js.map +1 -0
  59. package/dist/mcp/mcp.controller.d.ts +8 -0
  60. package/dist/mcp/mcp.controller.d.ts.map +1 -0
  61. package/dist/mcp/mcp.controller.js +54 -0
  62. package/dist/mcp/mcp.controller.js.map +1 -0
  63. package/dist/mcp/mcp.module.d.ts +3 -0
  64. package/dist/mcp/mcp.module.d.ts.map +1 -0
  65. package/dist/mcp/mcp.module.js +49 -0
  66. package/dist/mcp/mcp.module.js.map +1 -0
  67. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +12 -0
  68. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -0
  69. package/dist/mcp/tools/core-settings.mcp-tools.js +66 -0
  70. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -0
  71. package/dist/mcp/tools/core-users.mcp-tools.d.ts +55 -0
  72. package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -0
  73. package/dist/mcp/tools/core-users.mcp-tools.js +295 -0
  74. package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -0
  75. package/dist/mcp/types/mcp-context.type.d.ts +7 -0
  76. package/dist/mcp/types/mcp-context.type.d.ts.map +1 -0
  77. package/dist/mcp/types/mcp-context.type.js +3 -0
  78. package/dist/mcp/types/mcp-context.type.js.map +1 -0
  79. package/dist/mcp-chat/dto/create-conversation.dto.d.ts +4 -0
  80. package/dist/mcp-chat/dto/create-conversation.dto.d.ts.map +1 -0
  81. package/dist/mcp-chat/dto/create-conversation.dto.js +22 -0
  82. package/dist/mcp-chat/dto/create-conversation.dto.js.map +1 -0
  83. package/dist/mcp-chat/dto/send-message.dto.d.ts +4 -0
  84. package/dist/mcp-chat/dto/send-message.dto.d.ts.map +1 -0
  85. package/dist/mcp-chat/dto/send-message.dto.js +22 -0
  86. package/dist/mcp-chat/dto/send-message.dto.js.map +1 -0
  87. package/dist/mcp-chat/mcp-chat.controller.d.ts +55 -0
  88. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -0
  89. package/dist/mcp-chat/mcp-chat.controller.js +90 -0
  90. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -0
  91. package/dist/mcp-chat/mcp-chat.module.d.ts +3 -0
  92. package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -0
  93. package/dist/mcp-chat/mcp-chat.module.js +30 -0
  94. package/dist/mcp-chat/mcp-chat.module.js.map +1 -0
  95. package/dist/mcp-chat/mcp-chat.service.d.ts +50 -0
  96. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -0
  97. package/dist/mcp-chat/mcp-chat.service.js +290 -0
  98. package/dist/mcp-chat/mcp-chat.service.js.map +1 -0
  99. package/dist/menu/menu.controller.d.ts +3 -3
  100. package/dist/menu/menu.service.d.ts +3 -3
  101. package/dist/profile/profile.controller.d.ts +2 -2
  102. package/dist/profile/profile.service.d.ts +2 -2
  103. package/dist/route/dto/create.dto.d.ts +6 -2
  104. package/dist/route/dto/create.dto.d.ts.map +1 -1
  105. package/dist/route/dto/create.dto.js +19 -1
  106. package/dist/route/dto/create.dto.js.map +1 -1
  107. package/dist/route/dto/update.dto.d.ts +4 -0
  108. package/dist/route/dto/update.dto.d.ts.map +1 -1
  109. package/dist/route/dto/update.dto.js +19 -3
  110. package/dist/route/dto/update.dto.js.map +1 -1
  111. package/dist/route/route.service.d.ts +1 -1
  112. package/dist/route/route.service.d.ts.map +1 -1
  113. package/dist/route/route.service.js +18 -11
  114. package/dist/route/route.service.js.map +1 -1
  115. package/dist/setting/setting.controller.d.ts +5 -5
  116. package/dist/setting/setting.service.d.ts +5 -5
  117. package/dist/user/user.controller.d.ts +3 -3
  118. package/dist/user/user.service.d.ts +6 -6
  119. package/hedhog/data/menu.yaml +40 -26
  120. package/hedhog/data/route.yaml +575 -152
  121. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +574 -0
  122. package/hedhog/frontend/messages/en.json +17 -0
  123. package/hedhog/frontend/messages/pt.json +17 -0
  124. package/hedhog/table/mcp_conversation.yaml +12 -0
  125. package/hedhog/table/mcp_conversation_message.yaml +23 -0
  126. package/hedhog/table/mcp_tool_call_log.yaml +22 -0
  127. package/hedhog/table/route.yaml +33 -22
  128. package/package.json +5 -4
  129. package/src/core.module.ts +14 -10
  130. package/src/index.ts +17 -10
  131. package/src/mcp/decorators/mcp-tool.decorator.ts +7 -0
  132. package/src/mcp/guards/mcp-auth.guard.ts +32 -0
  133. package/src/mcp/guards/mcp-permission.guard.ts +51 -0
  134. package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -0
  135. package/src/mcp/mcp-audit.service.ts +36 -0
  136. package/src/mcp/mcp-registry.service.ts +69 -0
  137. package/src/mcp/mcp-server.service.ts +87 -0
  138. package/src/mcp/mcp.controller.ts +30 -0
  139. package/src/mcp/mcp.module.ts +36 -0
  140. package/src/mcp/tools/core-settings.mcp-tools.ts +53 -0
  141. package/src/mcp/tools/core-users.mcp-tools.ts +267 -0
  142. package/src/mcp/types/mcp-context.type.ts +6 -0
  143. package/src/mcp-chat/dto/create-conversation.dto.ts +7 -0
  144. package/src/mcp-chat/dto/send-message.dto.ts +7 -0
  145. package/src/mcp-chat/mcp-chat.controller.ts +60 -0
  146. package/src/mcp-chat/mcp-chat.module.ts +17 -0
  147. package/src/mcp-chat/mcp-chat.service.ts +332 -0
  148. package/src/route/dto/create.dto.ts +32 -14
  149. package/src/route/dto/update.dto.ts +31 -16
  150. package/src/route/route.service.ts +23 -12
@@ -1,22 +1,33 @@
1
- columns:
2
- - type: pk
3
- - name: url
4
- - name: method
5
- type: enum
6
- enum:
7
- - GET
8
- - POST
9
- - PATCH
10
- - DELETE
11
- - PUT
12
- - OPTIONS
13
- - HEAD
14
- - name: description
15
- isNullable: true
16
- - type: created_at
17
- - type: updated_at
18
- indices:
19
- - columns:
20
- - url
21
- - method
22
- isUnique: true
1
+ columns:
2
+ - type: pk
3
+ - name: type
4
+ type: enum
5
+ enum:
6
+ - HTTP
7
+ - MCP
8
+ default: HTTP
9
+ - name: name
10
+ isNullable: true
11
+ - name: url
12
+ isNullable: true
13
+ - name: method
14
+ type: enum
15
+ enum:
16
+ - GET
17
+ - POST
18
+ - PATCH
19
+ - DELETE
20
+ - PUT
21
+ - OPTIONS
22
+ - HEAD
23
+ isNullable: true
24
+ - name: tool_name
25
+ isNullable: true
26
+ - name: description
27
+ isNullable: true
28
+ - type: created_at
29
+ - type: updated_at
30
+ indices:
31
+ - columns:
32
+ - tool_name
33
+ isUnique: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.328",
3
+ "version": "0.0.329",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -30,12 +30,13 @@
30
30
  "sharp": "^0.34.2",
31
31
  "speakeasy": "^2.0.0",
32
32
  "uuid": "^11.1.0",
33
- "@hed-hog/api-locale": "0.0.14",
33
+ "@modelcontextprotocol/sdk": "^1.12.0",
34
34
  "@hed-hog/api": "0.0.8",
35
+ "@hed-hog/api-locale": "0.0.14",
35
36
  "@hed-hog/api-pagination": "0.0.7",
36
- "@hed-hog/api-prisma": "0.0.6",
37
+ "@hed-hog/api-mail": "0.0.9",
37
38
  "@hed-hog/api-types": "0.0.1",
38
- "@hed-hog/api-mail": "0.0.9"
39
+ "@hed-hog/api-prisma": "0.0.6"
39
40
  },
40
41
  "exports": {
41
42
  ".": {
@@ -8,6 +8,8 @@ import { forwardRef, Global, Inject, MiddlewareConsumer, Module, NestModule, OnM
8
8
  import { ConfigModule } from '@nestjs/config';
9
9
  import { APP_INTERCEPTOR, Reflector } from '@nestjs/core';
10
10
  import { AiModule } from './ai/ai.module';
11
+ import { McpModule } from './mcp/mcp.module';
12
+ import { McpChatModule } from './mcp-chat/mcp-chat.module';
11
13
  import { AuthModule } from './auth/auth.module';
12
14
  import { SystemModule } from './core/system.module';
13
15
  import { DashboardModule } from './dashboard/dashboard.module';
@@ -29,8 +31,8 @@ import { SessionModule } from './session/session.module';
29
31
  import { SettingModule } from './setting/setting.module';
30
32
  import { SettingService } from './setting/setting.service';
31
33
  import { TaskModule } from './task/task.module';
32
- import { UserModule } from './user/user.module';
33
- import { WebhookIntegrationModule } from './webhook-integration/webhook-integration.module';
34
+ import { UserModule } from './user/user.module';
35
+ import { WebhookIntegrationModule } from './webhook-integration/webhook-integration.module';
34
36
  import { IsEmailWithSettingsConstraint } from './validators/is-email-with-settings.validator';
35
37
  import { IsPinCodeWithSettingConstraint } from './validators/is-pin-code-with-setting.validator';
36
38
  import { IsStrongPasswordWithSettingsConstraint } from './validators/is-strong-password-with-settings.validator';
@@ -64,10 +66,12 @@ import { ValidatorServiceLocator } from './validators/service-locator';
64
66
  forwardRef(() => SecurityModule),
65
67
  forwardRef(() => ProfileModule),
66
68
  forwardRef(() => SessionModule),
67
- forwardRef(() => OAuthModule),
68
- forwardRef(() => IntegrationModule),
69
- forwardRef(() => WebhookIntegrationModule),
70
- ],
69
+ forwardRef(() => OAuthModule),
70
+ forwardRef(() => IntegrationModule),
71
+ forwardRef(() => WebhookIntegrationModule),
72
+ forwardRef(() => McpModule),
73
+ forwardRef(() => McpChatModule),
74
+ ],
71
75
  exports: [
72
76
  UserModule,
73
77
  AuthModule,
@@ -81,10 +85,10 @@ import { ValidatorServiceLocator } from './validators/service-locator';
81
85
  SettingModule,
82
86
  FileModule,
83
87
  SessionModule,
84
- AiModule,
85
- IntegrationModule,
86
- WebhookIntegrationModule,
87
- IsStrongPasswordWithSettingsConstraint,
88
+ AiModule,
89
+ IntegrationModule,
90
+ WebhookIntegrationModule,
91
+ IsStrongPasswordWithSettingsConstraint,
88
92
  IsEmailWithSettingsConstraint,
89
93
  IsPinCodeWithSettingConstraint,
90
94
  ],
package/src/index.ts CHANGED
@@ -29,16 +29,16 @@ export * from './file/file.service';
29
29
  export * from './setting/setting.module';
30
30
  export * from './setting/setting.service';
31
31
 
32
- export * from './ai/ai.module';
33
- export * from './ai/ai.service';
34
-
35
- export * from './webhook-integration/webhook-integration.module';
36
- export * from './webhook-integration/integration-event-catalog.service';
37
- export * from './webhook-integration/webhook-integration.service';
38
- export * from './webhook-integration/event-webhook.service';
39
-
40
- // Integration Module
41
- export * from './integration/integration.module';
32
+ export * from './ai/ai.module';
33
+ export * from './ai/ai.service';
34
+
35
+ export * from './webhook-integration/webhook-integration.module';
36
+ export * from './webhook-integration/integration-event-catalog.service';
37
+ export * from './webhook-integration/webhook-integration.service';
38
+ export * from './webhook-integration/event-webhook.service';
39
+
40
+ // Integration Module
41
+ export * from './integration/integration.module';
42
42
  export * from './integration/services/domain-event.publisher';
43
43
  export * from './integration/services/event-subscriber.registry';
44
44
  export * from './integration/services/inbox.service';
@@ -57,6 +57,13 @@ export * from './validators/is-email-with-settings.validator';
57
57
  export * from './validators/is-pin-code-with-setting.validator';
58
58
  export * from './validators/is-strong-password-with-settings.validator';
59
59
 
60
+ // MCP Module
61
+ export * from './mcp/mcp.module';
62
+ export * from './mcp/mcp-registry.service';
63
+ export * from './mcp/decorators/mcp-tool.decorator';
64
+ export * from './mcp/interfaces/mcp-tool-definition.interface';
65
+ export * from './mcp/types/mcp-context.type';
66
+
60
67
  // Utilities
61
68
  export * from './utils/locale-context';
62
69
 
@@ -0,0 +1,7 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+ import { McpToolDefinition } from '../interfaces/mcp-tool-definition.interface';
3
+
4
+ export const MCP_TOOL_METADATA = 'mcp:tool';
5
+
6
+ export const McpTool = (definition: McpToolDefinition): MethodDecorator =>
7
+ SetMetadata(MCP_TOOL_METADATA, definition);
@@ -0,0 +1,32 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ Injectable,
5
+ UnauthorizedException,
6
+ } from '@nestjs/common';
7
+ import { Request } from 'express';
8
+ import { TokenService } from '../../token/token.service';
9
+
10
+ @Injectable()
11
+ export class McpAuthGuard implements CanActivate {
12
+ constructor(private readonly tokenService: TokenService) {}
13
+
14
+ async canActivate(context: ExecutionContext): Promise<boolean> {
15
+ const request = context.switchToHttp().getRequest<Request>();
16
+ const locale =
17
+ request.headers['accept-language']?.split(',')[0]?.split('-')[0] ?? 'en';
18
+
19
+ const token = this.extractToken(request);
20
+ if (!token) {
21
+ throw new UnauthorizedException('MCP: Bearer token required');
22
+ }
23
+
24
+ request['auth'] = await this.tokenService.verify(locale, token);
25
+ return true;
26
+ }
27
+
28
+ private extractToken(request: Request): string | undefined {
29
+ const [type, token] = request.headers.authorization?.split(' ') ?? [];
30
+ return type === 'Bearer' ? token : undefined;
31
+ }
32
+ }
@@ -0,0 +1,51 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import {
3
+ CanActivate,
4
+ ExecutionContext,
5
+ ForbiddenException,
6
+ Injectable,
7
+ } from '@nestjs/common';
8
+ import { Request } from 'express';
9
+
10
+ @Injectable()
11
+ export class McpPermissionGuard implements CanActivate {
12
+ constructor(private readonly prisma: PrismaService) {}
13
+
14
+ async canActivate(context: ExecutionContext): Promise<boolean> {
15
+ const request = context.switchToHttp().getRequest<Request>();
16
+
17
+ // Only POST requests carry tool invocations
18
+ if (request.method !== 'POST') return true;
19
+
20
+ const body = request.body as any;
21
+ const toolName: string | undefined =
22
+ body?.method === 'tools/call' ? body?.params?.name : undefined;
23
+
24
+ // tools/list, initialize, ping, etc. — no per-tool permission check needed
25
+ if (!toolName) return true;
26
+
27
+ const userId = (request as any)?.auth?.sub;
28
+
29
+ const count = await this.prisma.route.count({
30
+ where: {
31
+ type: 'MCP' as any,
32
+ tool_name: toolName,
33
+ role_route: {
34
+ some: {
35
+ role: {
36
+ role_user: {
37
+ some: { user_id: userId },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ });
44
+
45
+ if (count === 0) {
46
+ throw new ForbiddenException(`Access denied to MCP tool '${toolName}'`);
47
+ }
48
+
49
+ return true;
50
+ }
51
+ }
@@ -0,0 +1,6 @@
1
+ export interface McpToolDefinition {
2
+ name: string;
3
+ description: string;
4
+ inputSchema?: Record<string, any>;
5
+ readOnly?: boolean;
6
+ }
@@ -0,0 +1,36 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable, Logger } from '@nestjs/common';
3
+
4
+ interface AuditRecord {
5
+ userId: number;
6
+ toolName: string;
7
+ input: Record<string, any>;
8
+ success: boolean;
9
+ error?: string;
10
+ durationMs: number;
11
+ }
12
+
13
+ @Injectable()
14
+ export class McpAuditService {
15
+ private readonly logger = new Logger(McpAuditService.name);
16
+
17
+ constructor(private readonly prisma: PrismaService) {}
18
+
19
+ async record(data: AuditRecord): Promise<void> {
20
+ const sanitized = JSON.stringify(data.input).slice(0, 1000);
21
+ try {
22
+ await this.prisma.mcp_tool_call_log.create({
23
+ data: {
24
+ user_id: data.userId || null,
25
+ tool_name: data.toolName,
26
+ input: sanitized,
27
+ success: data.success,
28
+ error: data.error ?? null,
29
+ duration_ms: data.durationMs,
30
+ },
31
+ });
32
+ } catch (e: any) {
33
+ this.logger.warn(`Failed to persist MCP audit record: ${e.message}`);
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,69 @@
1
+ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
2
+ import { DiscoveryService } from '@nestjs/core';
3
+ import { Reflector } from '@nestjs/core';
4
+ import { MCP_TOOL_METADATA } from './decorators/mcp-tool.decorator';
5
+ import { McpToolDefinition } from './interfaces/mcp-tool-definition.interface';
6
+ import { McpContext } from './types/mcp-context.type';
7
+
8
+ export interface McpToolEntry {
9
+ definition: McpToolDefinition;
10
+ handler: (args: Record<string, any>, context: McpContext) => Promise<any>;
11
+ }
12
+
13
+ @Injectable()
14
+ export class McpRegistryService implements OnModuleInit {
15
+ private readonly logger = new Logger(McpRegistryService.name);
16
+ private readonly tools = new Map<string, McpToolEntry>();
17
+
18
+ constructor(
19
+ private readonly discovery: DiscoveryService,
20
+ private readonly reflector: Reflector,
21
+ ) {}
22
+
23
+ onModuleInit(): void {
24
+ const providers = this.discovery.getProviders();
25
+
26
+ for (const wrapper of providers) {
27
+ const { instance } = wrapper;
28
+ if (!instance || typeof instance !== 'object') continue;
29
+
30
+ const prototype = Object.getPrototypeOf(instance);
31
+ if (!prototype) continue;
32
+
33
+ const methodNames = Object.getOwnPropertyNames(prototype).filter((key) => {
34
+ if (key === 'constructor') return false;
35
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
36
+ return descriptor !== undefined && typeof descriptor.value === 'function';
37
+ });
38
+
39
+ for (const methodName of methodNames) {
40
+ const definition = this.reflector.get<McpToolDefinition>(
41
+ MCP_TOOL_METADATA,
42
+ prototype[methodName],
43
+ );
44
+
45
+ if (!definition) continue;
46
+
47
+ this.logger.log(`Registered MCP tool: ${definition.name}`);
48
+ this.tools.set(definition.name, {
49
+ definition,
50
+ handler: (instance as any)[methodName].bind(instance),
51
+ });
52
+ }
53
+ }
54
+
55
+ this.logger.log(`McpRegistryService: ${this.tools.size} tools registered`);
56
+ }
57
+
58
+ getAll(): McpToolEntry[] {
59
+ return Array.from(this.tools.values());
60
+ }
61
+
62
+ get(toolName: string): McpToolEntry | undefined {
63
+ return this.tools.get(toolName);
64
+ }
65
+
66
+ has(toolName: string): boolean {
67
+ return this.tools.has(toolName);
68
+ }
69
+ }
@@ -0,0 +1,87 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+ import { Injectable } from '@nestjs/common';
8
+ import { Request, Response } from 'express';
9
+ import { McpAuditService } from './mcp-audit.service';
10
+ import { McpRegistryService } from './mcp-registry.service';
11
+ import { McpContext } from './types/mcp-context.type';
12
+
13
+ @Injectable()
14
+ export class McpServerService {
15
+ constructor(
16
+ private readonly registry: McpRegistryService,
17
+ private readonly audit: McpAuditService,
18
+ ) {}
19
+
20
+ async handleRequest(req: Request, res: Response, context: McpContext): Promise<void> {
21
+ const server = this.buildServer(context);
22
+ const transport = new StreamableHTTPServerTransport({
23
+ sessionIdGenerator: undefined,
24
+ });
25
+ await server.connect(transport);
26
+ await transport.handleRequest(req, res, req.body);
27
+ }
28
+
29
+ private buildServer(context: McpContext): Server {
30
+ const server = new Server(
31
+ { name: 'hedhog-mcp', version: '1.0.0' },
32
+ { capabilities: { tools: {} } },
33
+ );
34
+
35
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
36
+ tools: this.registry.getAll().map((e) => ({
37
+ name: e.definition.name,
38
+ description: e.definition.description,
39
+ inputSchema: e.definition.inputSchema ?? {
40
+ type: 'object',
41
+ properties: {},
42
+ },
43
+ })),
44
+ }));
45
+
46
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
47
+ const toolName = request.params.name;
48
+ const args = (request.params.arguments ?? {}) as Record<string, any>;
49
+ const entry = this.registry.get(toolName);
50
+
51
+ if (!entry) {
52
+ return {
53
+ isError: true,
54
+ content: [{ type: 'text', text: `Tool '${toolName}' not found` }],
55
+ };
56
+ }
57
+
58
+ const start = Date.now();
59
+ try {
60
+ const result = await entry.handler(args, { ...context, toolName });
61
+ await this.audit.record({
62
+ userId: context.userId,
63
+ toolName,
64
+ input: args,
65
+ success: true,
66
+ durationMs: Date.now() - start,
67
+ });
68
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
69
+ } catch (e: any) {
70
+ await this.audit.record({
71
+ userId: context.userId,
72
+ toolName,
73
+ input: args,
74
+ success: false,
75
+ error: e.message,
76
+ durationMs: Date.now() - start,
77
+ });
78
+ return {
79
+ isError: true,
80
+ content: [{ type: 'text', text: e.message }],
81
+ };
82
+ }
83
+ });
84
+
85
+ return server;
86
+ }
87
+ }
@@ -0,0 +1,30 @@
1
+ import { NoRole } from '@hed-hog/api';
2
+ import { All, Controller, Req, Res, UseGuards } from '@nestjs/common';
3
+ import { Request, Response } from 'express';
4
+ import { McpAuthGuard } from './guards/mcp-auth.guard';
5
+ import { McpPermissionGuard } from './guards/mcp-permission.guard';
6
+ import { McpServerService } from './mcp-server.service';
7
+
8
+ @NoRole()
9
+ @UseGuards(McpAuthGuard, McpPermissionGuard)
10
+ @Controller('mcp')
11
+ export class McpController {
12
+ constructor(private readonly mcpServer: McpServerService) {}
13
+
14
+ @All()
15
+ async handle(@Req() req: Request, @Res() res: Response): Promise<void> {
16
+ const auth = (req as any).auth;
17
+ const locale =
18
+ req.headers['accept-language']?.split(',')[0]?.split('-')[0] ?? 'en';
19
+ const body = req.body as any;
20
+ const toolName: string =
21
+ body?.method === 'tools/call' ? (body?.params?.name ?? '') : '';
22
+
23
+ await this.mcpServer.handleRequest(req, res, {
24
+ userId: auth.sub,
25
+ sessionId: String(auth.sessionId),
26
+ locale,
27
+ toolName,
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,36 @@
1
+ import { PrismaModule } from '@hed-hog/api-prisma';
2
+ import { Module, forwardRef } from '@nestjs/common';
3
+ import { DiscoveryModule } from '@nestjs/core';
4
+ import { SettingModule } from '../setting/setting.module';
5
+ import { TokenModule } from '../token/token.module';
6
+ import { UserModule } from '../user/user.module';
7
+ import { McpAuthGuard } from './guards/mcp-auth.guard';
8
+ import { McpPermissionGuard } from './guards/mcp-permission.guard';
9
+ import { McpAuditService } from './mcp-audit.service';
10
+ import { McpRegistryService } from './mcp-registry.service';
11
+ import { McpServerService } from './mcp-server.service';
12
+ import { McpController } from './mcp.controller';
13
+ import { CoreSettingsMcpTools } from './tools/core-settings.mcp-tools';
14
+ import { CoreUsersMcpTools } from './tools/core-users.mcp-tools';
15
+
16
+ @Module({
17
+ imports: [
18
+ DiscoveryModule,
19
+ forwardRef(() => PrismaModule),
20
+ forwardRef(() => TokenModule),
21
+ forwardRef(() => SettingModule),
22
+ forwardRef(() => UserModule),
23
+ ],
24
+ controllers: [McpController],
25
+ providers: [
26
+ McpRegistryService,
27
+ McpServerService,
28
+ McpAuditService,
29
+ McpAuthGuard,
30
+ McpPermissionGuard,
31
+ CoreSettingsMcpTools,
32
+ CoreUsersMcpTools,
33
+ ],
34
+ exports: [McpRegistryService, McpServerService],
35
+ })
36
+ export class McpModule {}
@@ -0,0 +1,53 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { SettingService } from '../../setting/setting.service';
3
+ import { McpTool } from '../decorators/mcp-tool.decorator';
4
+ import { McpContext } from '../types/mcp-context.type';
5
+
6
+ @Injectable()
7
+ export class CoreSettingsMcpTools {
8
+ constructor(private readonly settingService: SettingService) {}
9
+
10
+ @McpTool({
11
+ name: 'core.settings.get',
12
+ description: 'Returns effective settings for the authenticated user',
13
+ inputSchema: { type: 'object', properties: {} },
14
+ readOnly: true,
15
+ })
16
+ async getEffectiveSettings(
17
+ _args: Record<string, any>,
18
+ context: McpContext,
19
+ ): Promise<any> {
20
+ return this.settingService.getEffectiveSettings(context.userId);
21
+ }
22
+
23
+ @McpTool({
24
+ name: 'core.settings.set',
25
+ description:
26
+ 'Updates a user-overridable setting value for the authenticated user. Only settings with user_override=true can be changed.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ slug: {
31
+ type: 'string',
32
+ description: 'The slug identifier of the setting to update',
33
+ },
34
+ value: {
35
+ type: 'string',
36
+ description: 'The new value for the setting',
37
+ },
38
+ },
39
+ required: ['slug', 'value'],
40
+ },
41
+ })
42
+ async setUserSetting(
43
+ args: { slug: string; value: string },
44
+ context: McpContext,
45
+ ): Promise<any> {
46
+ return this.settingService.setSettingUserValue(
47
+ context.locale,
48
+ context.userId,
49
+ args.slug,
50
+ args.value,
51
+ );
52
+ }
53
+ }