From e629e749fca239260e76b10909e54fc7862919b5 Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:21:20 +0300 Subject: [PATCH 01/12] feat(mcp): add mcp-server service to docker-compose and update dependencies --- docker/docker-compose.yml | 15 ++++++ package-lock.json | 100 +++++++++++++++++++++++++++++++++----- package.json | 2 + 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 07138fc..ffa961b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -19,6 +19,21 @@ services: networks: - nestjs-todo-network + mcp-server: + build: + context: .. + dockerfile: ./docker/Dockerfile + command: ['npm', 'run', 'mcp:dev'] + volumes: + - ../src:/usr/src/app/src + depends_on: + - mongo + - redis-db + env_file: + - ../.env + networks: + - nestjs-todo-network + mongo: image: mongo container_name: todo-playground-mongo diff --git a/package-lock.json b/package-lock.json index 5fc13ad..cfe2781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@elastic/elasticsearch": "9.0.3", + "@modelcontextprotocol/sdk": "1.18.1", "@nestjs-modules/ioredis": "2.0.2", "@nestjs/bullmq": "11.0.3", "@nestjs/common": "11.1.0", @@ -29,7 +30,7 @@ "dotenv": "16.5.0", "ioredis": "5.7.0", "mongoose": "8.14.1", - "redlock": "^5.0.0-beta.2", + "redlock": "5.0.0-beta.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.2" }, @@ -2057,6 +2058,29 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", + "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", @@ -5151,7 +5175,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -6721,7 +6744,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7382,6 +7404,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7481,6 +7524,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -7524,7 +7582,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -7575,7 +7632,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -8737,7 +8793,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9584,7 +9639,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -10830,7 +10884,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10949,6 +11002,15 @@ "@napi-rs/nice": "^1.0.1" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -11754,7 +11816,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11767,7 +11828,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13098,7 +13158,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -13446,7 +13505,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13710,6 +13768,24 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index 5f1e386..2f6b5ae 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { + "mcp:dev": "ts-node -r tsconfig-paths/register src/mcp-server.ts", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", @@ -21,6 +22,7 @@ }, "dependencies": { "@elastic/elasticsearch": "9.0.3", + "@modelcontextprotocol/sdk": "1.18.1", "@nestjs-modules/ioredis": "2.0.2", "@nestjs/bullmq": "11.0.3", "@nestjs/common": "11.1.0", From ee57dff05556aef18348b8ed5d6429318fac1dff Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:21:47 +0300 Subject: [PATCH 02/12] feat(mcp): add MCP tool not found and category not supported exceptions, and define MCP tool constants --- src/core/error/error-code.ts | 3 ++ src/core/error/exception/index.ts | 2 + .../mcp-category-not-supported.exception.ts | 12 ++++++ .../exception/mcp-tool-not-found.exception.ts | 12 ++++++ src/core/interface/index.ts | 1 + src/core/interface/mcp/index.ts | 1 + src/core/interface/mcp/mcp-tool.const.ts | 38 +++++++++++++++++++ 7 files changed, 69 insertions(+) create mode 100644 src/core/error/exception/mcp-category-not-supported.exception.ts create mode 100644 src/core/error/exception/mcp-tool-not-found.exception.ts create mode 100644 src/core/interface/mcp/index.ts create mode 100644 src/core/interface/mcp/mcp-tool.const.ts diff --git a/src/core/error/error-code.ts b/src/core/error/error-code.ts index 2d5ab11..2d8d71d 100644 --- a/src/core/error/error-code.ts +++ b/src/core/error/error-code.ts @@ -6,6 +6,9 @@ export enum ErrorCode { BAD_INPUT = 400100, NICKNAME_ALREADY_TAKEN = 400101, INVALID_CREDENTIALS = 400103, + //MCP + MCP_TOOL_NOT_FOUND = 400200, + MCP_CATEGORY_NOT_SUPPORTED = 400201, // 401 UNAUTHORIZED = 40100, diff --git a/src/core/error/exception/index.ts b/src/core/error/exception/index.ts index 86f480f..afe4a9d 100644 --- a/src/core/error/exception/index.ts +++ b/src/core/error/exception/index.ts @@ -10,3 +10,5 @@ export * from './unauthorized.exception'; export * from './invalid-refresh-token.exception'; export * from './todo-not-found.exception'; export * from './todo-deletion-pending.exception'; +export * from './mcp-tool-not-found.exception'; +export * from './mcp-category-not-supported.exception'; diff --git a/src/core/error/exception/mcp-category-not-supported.exception.ts b/src/core/error/exception/mcp-category-not-supported.exception.ts new file mode 100644 index 0000000..1406b65 --- /dev/null +++ b/src/core/error/exception/mcp-category-not-supported.exception.ts @@ -0,0 +1,12 @@ +import { ErrorCode } from '../error-code'; +import { BadInputException } from './bad-input.exception'; + +export class McpCategoryNotSupportedException extends BadInputException { + constructor(category: string) { + super( + `MCP tool category '${category}' is not supported`, + ErrorCode.MCP_CATEGORY_NOT_SUPPORTED, + `Category '${category}' is not available`, + ); + } +} diff --git a/src/core/error/exception/mcp-tool-not-found.exception.ts b/src/core/error/exception/mcp-tool-not-found.exception.ts new file mode 100644 index 0000000..5ec5378 --- /dev/null +++ b/src/core/error/exception/mcp-tool-not-found.exception.ts @@ -0,0 +1,12 @@ +import { ErrorCode } from '../error-code'; +import { NotFoundException } from './not-found.exception'; + +export class McpToolNotFoundException extends NotFoundException { + constructor(toolName: string) { + super( + `MCP tool '${toolName}' not found`, + ErrorCode.MCP_TOOL_NOT_FOUND, + `Tool '${toolName}' is not available`, + ); + } +} diff --git a/src/core/interface/index.ts b/src/core/interface/index.ts index 8ac571c..6de9136 100644 --- a/src/core/interface/index.ts +++ b/src/core/interface/index.ts @@ -1,3 +1,4 @@ export * from './environment.interface'; export * from './mongo-model'; export * from './bullmq'; +export * from './mcp'; diff --git a/src/core/interface/mcp/index.ts b/src/core/interface/mcp/index.ts new file mode 100644 index 0000000..2318c5b --- /dev/null +++ b/src/core/interface/mcp/index.ts @@ -0,0 +1 @@ +export * from './mcp-tool.const'; diff --git a/src/core/interface/mcp/mcp-tool.const.ts b/src/core/interface/mcp/mcp-tool.const.ts new file mode 100644 index 0000000..7b4d2eb --- /dev/null +++ b/src/core/interface/mcp/mcp-tool.const.ts @@ -0,0 +1,38 @@ +export const McpTool = { + AUTH_REGISTER: { + name: 'auth_register', + description: 'Register a new user', + category: 'auth' as const, + }, + AUTH_LOGIN: { + name: 'auth_login', + description: 'Login user and get access token', + category: 'auth' as const, + }, + TODO_CREATE: { + name: 'todo_create', + description: 'Create a new todo item', + category: 'todo' as const, + }, + TODO_LIST: { + name: 'todo_list', + description: 'List user todos with pagination', + category: 'todo' as const, + }, + TODO_UPDATE: { + name: 'todo_update', + description: 'Update an existing todo', + category: 'todo' as const, + }, + TODO_DELETE: { + name: 'todo_delete', + description: 'Delete a todo item', + category: 'todo' as const, + }, +} as const; + +// Type utilities +export type McpToolKey = keyof typeof McpTool; +export type McpToolValue = (typeof McpTool)[McpToolKey]; +export type McpToolName = McpToolValue['name']; +export type McpToolCategory = McpToolValue['category']; From 6f0c3944e43d462140c2c2a6d148208254d60ab9 Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:25:32 +0300 Subject: [PATCH 03/12] feat(mcp): implement MCP server and authentication tools with request handlers --- src/app.module.ts | 2 + src/mcp-server.ts | 13 ++++ src/modules/mcp/mcp.module.ts | 11 +++ src/modules/mcp/server/mcp.server.ts | 86 ++++++++++++++++++++++ src/modules/mcp/tools/auth.tools.ts | 106 +++++++++++++++++++++++++++ 5 files changed, 218 insertions(+) create mode 100644 src/mcp-server.ts create mode 100644 src/modules/mcp/mcp.module.ts create mode 100644 src/modules/mcp/server/mcp.server.ts create mode 100644 src/modules/mcp/tools/auth.tools.ts diff --git a/src/app.module.ts b/src/app.module.ts index 0994c34..5291fc2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { Environment } from './core/interface'; import { TodoModule } from './modules/todo/todo.module'; import { RabbitmqModule } from './modules/utils/rabbitmq/rabbitmq.module'; import { LockModule } from './core/cache/lock/lock.module'; +import { McpModule } from './modules/mcp/mcp.module'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { LockModule } from './core/cache/lock/lock.module'; TodoModule, RabbitmqModule, LockModule, + McpModule, ], controllers: [], diff --git a/src/mcp-server.ts b/src/mcp-server.ts new file mode 100644 index 0000000..5ab9ea2 --- /dev/null +++ b/src/mcp-server.ts @@ -0,0 +1,13 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { McpServer } from './modules/mcp/server/mcp.server'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + const mcpServer = app.get(McpServer); + + await mcpServer.start(); +} + +bootstrap(); diff --git a/src/modules/mcp/mcp.module.ts b/src/modules/mcp/mcp.module.ts new file mode 100644 index 0000000..f616678 --- /dev/null +++ b/src/modules/mcp/mcp.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { McpServer } from './server/mcp.server'; +import { AuthModule } from '../auth/auth.module'; +import { UserModule } from '../user/user.module'; + +@Module({ + imports: [AuthModule, UserModule], + providers: [McpServer], + exports: [McpServer], +}) +export class McpModule {} diff --git a/src/modules/mcp/server/mcp.server.ts b/src/modules/mcp/server/mcp.server.ts new file mode 100644 index 0000000..7824b52 --- /dev/null +++ b/src/modules/mcp/server/mcp.server.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@nestjs/common'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { AuthService } from '../../auth/service/auth.service'; +import { AuthTools } from '../tools/auth.tools'; +import { UserService } from 'src/modules/user/service/user.service'; +import { McpTool } from 'src/core/interface/mcp/mcp-tool.const'; +import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; +import { McpCategoryNotSupportedException } from 'src/core/error/exception/mcp-category-not-supported.exception'; + +@Injectable() +export class McpServer { + private server: Server; + private authTools: AuthTools; + + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + ) { + this.authTools = new AuthTools(this.authService, this.userService); + this.initializeServer(); + } + + private initializeServer() { + this.server = new Server( + { + name: 'nestjs-todo-mcp', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + this.setupToolHandlers(); + } + + private setupToolHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [...this.authTools.getToolDefinitions()], + }; + }); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + // Category-based routing + const allTools = Object.values(McpTool); + const tool = allTools.find((t) => t.name === name); + + if (!tool) { + throw new McpToolNotFoundException(name); + } + + if (tool.category === 'auth') { + return await this.authTools.handleToolCall(name, args); + } + + throw new McpCategoryNotSupportedException(tool.category); + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}`, + }, + ], + isError: true, + }; + } + }); + } + + async start() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + } +} diff --git a/src/modules/mcp/tools/auth.tools.ts b/src/modules/mcp/tools/auth.tools.ts new file mode 100644 index 0000000..558eeda --- /dev/null +++ b/src/modules/mcp/tools/auth.tools.ts @@ -0,0 +1,106 @@ +import { AuthService } from '../../auth/service/auth.service'; +import { UserService } from '../../user/service/user.service'; +import { SignInDto } from '../../auth/dto'; +import { CreateUserDto } from '../../user/dto'; +import { McpTool } from 'src/core/interface'; +import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; + +export class AuthTools { + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + ) {} + + getToolDefinitions() { + return [ + { + name: McpTool.AUTH_REGISTER.name, + description: McpTool.AUTH_REGISTER.description, + inputSchema: { + type: 'object', + properties: { + fullname: { + type: 'string', + description: 'User full name', + }, + nickname: { + type: 'string', + description: 'User nickname', + }, + password: { + type: 'string', + description: 'User password', + }, + }, + required: ['fullname', 'nickname', 'password'], + }, + }, + { + name: McpTool.AUTH_LOGIN.name, + description: McpTool.AUTH_LOGIN.description, + inputSchema: { + type: 'object', + properties: { + nickname: { + type: 'string', + description: 'User nickname', + }, + password: { + type: 'string', + description: 'User password', + }, + }, + required: ['nickname', 'password'], + }, + }, + ]; + } + + async handleToolCall(toolName: string, args: any) { + switch (toolName) { + case McpTool.AUTH_REGISTER.name: + return await this.register(args); + case McpTool.AUTH_LOGIN.name: + return await this.login(args); + default: + throw new McpToolNotFoundException(toolName); + } + } + + private async register(args: CreateUserDto) { + const result = await this.userService.create(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: 'User registered successfully', + data: { + id: result.id, + }, + }), + }, + ], + }; + } + + private async login(args: SignInDto) { + const result = await this.authService.signIn(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Login successful', + data: { + accessToken: result.accessToken, + refreshToken: result.refreshToken, + }, + }), + }, + ], + }; + } +} From 4d06d2480b71d28bc25f65a04bdfd2a7490b81bf Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:01:06 +0300 Subject: [PATCH 04/12] docs: update README to enhance project description and detail MCP integration --- README.md | 227 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 208 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index cf7e75c..521e7e1 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,67 @@ # πŸ“ NestJS Todo Playground -A simple and extensible Todo backend playground built with [NestJS](https://nestjs.com/), MongoDB (Mongoose), and TypeScript. This project demonstrates modular architecture, DTO validation, custom error handling, and includes ready-to-use Docker and test setups. +A comprehensive Todo backend playground built with [NestJS](https://nestjs.com/), showcasing modern backend architecture with microservices patterns, event-driven design, and AI integration. This project demonstrates advanced NestJS features, distributed systems concepts, and production-ready patterns. --- ## πŸš€ Features -- Modular structure with NestJS -- Authentication (JWT tokens) -- `MongoDB` integration using Mongoose -- User registration with unique nickname validation -- DTO validation with class-validator -- Custom error and exception handling -- Ready-to-use `integration tests` (Jest & Supertest) -- `Docker support` for easy development and deployment +### Core Backend Features + +- **Modular NestJS Architecture** with clean separation of concerns +- **JWT Authentication** with access & refresh tokens +- **MongoDB Integration** using Mongoose ODM +- **Redis Caching & Session Management** +- **DTO Validation** with class-validator +- **Custom Exception Handling** with structured error responses +- **Integration Testing Suite** (Jest & Supertest) + +### Advanced Features + +- **πŸ”„ BullMQ Job Queue** - Delayed todo deletion with rollback support +- **πŸ” Elasticsearch Integration** - Full-text search capabilities +- **🐰 RabbitMQ Event System** - Event-driven architecture for data synchronization +- **πŸ”’ RedLock Distributed Locking** - Race condition protection +- **⚑ Real-time Search** - Instant todo search with Elasticsearch +- **🐳 Docker-Compose Setup** - Multi-container development environment + +### AI & Integration + +- **πŸ€– MCP (Model Context Protocol)** - Claude Desktop integration +- **πŸ”§ Natural Language Interface** - Control your app via AI commands +- **πŸ“‘ JSON-RPC Protocol** - Standardized AI-to-backend communication + +--- + +## πŸ› οΈ Tech Stack + +### Backend Framework + +- **[NestJS](https://nestjs.com/)** - Progressive Node.js framework +- **TypeScript** - Type-safe JavaScript +- **Express.js** - Web framework (underlying NestJS) + +### Databases & Storage + +- **[MongoDB](https://www.mongodb.com/)** - Document database with Mongoose ODM +- **[Redis](https://redis.io/)** - In-memory caching & session storage +- **[Elasticsearch](https://www.elastic.co/)** - Full-text search engine + +### Message Queues & Events + +- **[BullMQ](https://bullmq.io/)** - Redis-based job queue for background tasks +- **[RabbitMQ](https://www.rabbitmq.com/)** - Message broker for event-driven architecture + +### DevOps & Infrastructure + +- **[Docker](https://www.docker.com/)** & **Docker Compose** - Containerization +- **[Jest](https://jestjs.io/)** - Testing framework with Supertest +- **[Swagger](https://swagger.io/)** - API documentation + +### AI Integration + +- **[Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/)** - AI integration standard +- **JSON-RPC** - Remote procedure call protocol --- @@ -29,7 +77,7 @@ npm install ```bash $ cd docker -$ docker compose up +$ docker compose up ``` --- @@ -38,7 +86,103 @@ $ docker compose up ```bash $ cd docker -$ docker compose down +$ docker compose down +``` + +--- + +## πŸ€– MCP (Model Context Protocol) Integration + +This project includes MCP server integration, allowing **Claude Desktop** to interact with your Todo application using natural language commands. + +### πŸƒβ€β™‚οΈ Running MCP Server + +The MCP server runs automatically when you start the Docker containers: + +```bash +$ cd docker +$ docker compose up +``` + +This starts both: + +- **HTTP API Server** (port 3000) - Regular REST API +- **MCP Server** (stdio) - Claude Desktop integration + +### πŸ”§ Claude Desktop Setup + +1. **Install Claude Desktop** from Anthropic + +2. **Create/Edit Claude Desktop config file**: + + - **macOS**: `~/.config/claude-desktop/claude_desktop_config.json` + - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + - **Linux**: `~/.config/claude-desktop/claude_desktop_config.json` + +3. **Add MCP server configuration**: + +```json +{ + "mcpServers": { + "nestjs-todo": { + "command": "docker", + "args": [ + "exec", + "-i", + "docker-mcp-server-1", + "node", + "dist/mcp-server.js" + ] + } + } +} +``` + +4. **Restart Claude Desktop** completely + +### 🎯 Available MCP Tools + +- `auth_register` - Register new user +- `auth_login` - User authentication and JWT token retrieval + +### πŸ’¬ Example Claude Desktop Commands + +Try these natural language commands in Claude Desktop: + +``` +Can you register a new user with: +- Full name: "John Doe" +- Nickname: "johndoe" +- Password: "secure123" +``` + +``` +Please login user "johndoe" with password "secure123" +``` + +``` +What MCP tools are available in this application? +``` + +### πŸ” Troubleshooting MCP + +**Check if MCP server is running:** + +```bash +docker ps | grep mcp-server +# Should show: docker-mcp-server-1 +``` + +**View MCP server logs:** + +```bash +docker logs docker-mcp-server-1 +``` + +**Restart only MCP server:** + +```bash +docker compose restart mcp-server ``` --- @@ -79,17 +223,57 @@ npm run test ## πŸ“¬ Example API Usage -Create a user: +### Authentication ```http +# Register User POST /user Content-Type: application/json { - "nickname": "your_nickname", - "fullname": "Your Name", - "password": "your_password" + "nickname": "johndoe", + "fullname": "John Doe", + "password": "secure123" +} + +# Login User +POST /auth/sign-in +Content-Type: application/json + +{ + "nickname": "johndoe", + "password": "secure123" +} +``` + +### Todo Management + +```http +# Create Todo +POST /todo +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Learn NestJS", + "description": "Build a comprehensive todo app" } + +# Get Todos +GET /todo?page=1&limit=10&completed=false +Authorization: Bearer + +# Search Todos (Elasticsearch) +GET /todo/search?q=NestJS +Authorization: Bearer + +# Delete Todo (with 4-second delay) +DELETE /todo/:id +Authorization: Bearer + +# Cancel Deletion (within 4 seconds) +POST /todo/:id/cancel-deletion +Authorization: Bearer ``` --- @@ -110,12 +294,17 @@ Pull requests and suggestions are welcome! ## πŸ“š Resources -- [NestJS Documentation](https://docs.nestjs.com) -- [Mongoose Docs](https://mongoosejs.com/) -- [Jest Docs](https://jestjs.io/) +- [NestJS Documentation](https://docs.nestjs.com) - Framework fundamentals +- [MongoDB & Mongoose](https://mongoosejs.com/) - Database & ODM +- [BullMQ Documentation](https://bullmq.io/) - Job queue system +- [RabbitMQ Tutorials](https://www.rabbitmq.com/tutorials.html) - Message broker +- [Elasticsearch Guide](https://www.elastic.co/guide/) - Search engine +- [Model Context Protocol](https://spec.modelcontextprotocol.io/) - AI integration standard +- [Docker Compose](https://docs.docker.com/compose/) - Multi-container applications +- [Jest Testing](https://jestjs.io/) - Testing framework --- ## πŸͺͺ License -This project is for educational purposes and is currently **unlicensed**. +This project is for educational purposes demonstrating advanced NestJS patterns and AI integration. Currently **unlicensed**. From 8a47c93715daa072a5e8efbb06bc2a25a350f505 Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:07:51 +0300 Subject: [PATCH 05/12] feat(mcp): update todo deletion descriptions and add cancellation functionality --- docker/docker-compose.yml | 1 + src/core/interface/mcp/mcp-tool.const.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ffa961b..9b00337 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -29,6 +29,7 @@ services: depends_on: - mongo - redis-db + - todo-playground-server env_file: - ../.env networks: diff --git a/src/core/interface/mcp/mcp-tool.const.ts b/src/core/interface/mcp/mcp-tool.const.ts index 7b4d2eb..c5eb7d3 100644 --- a/src/core/interface/mcp/mcp-tool.const.ts +++ b/src/core/interface/mcp/mcp-tool.const.ts @@ -26,7 +26,14 @@ export const McpTool = { }, TODO_DELETE: { name: 'todo_delete', - description: 'Delete a todo item', + description: + 'Schedule todo for deletion (delayed 4 seconds) - returns pending status, user can cancel within this time', + category: 'todo' as const, + }, + TODO_CANCEL_DELETION: { + name: 'todo_cancel_deletion', + description: + 'Cancel pending todo deletion and restore the todo (only works within 4 seconds of deletion request)', category: 'todo' as const, }, } as const; From 8739506def880c5df753f2450983888235cc2ccf Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:17:54 +0300 Subject: [PATCH 06/12] feat: add class-validator-jsonschema dependency to enhance validation capabilities --- package-lock.json | 46 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index cfe2781..5b5d02c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "bullmq": "5.58.5", "class-transformer": "0.5.1", "class-validator": "0.14.1", + "class-validator-jsonschema": "^5.1.0", "dotenv": "16.5.0", "ioredis": "5.7.0", "mongoose": "8.14.1", @@ -6290,6 +6291,23 @@ "validator": "^13.9.0" } }, + "node_modules/class-validator-jsonschema": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/class-validator-jsonschema/-/class-validator-jsonschema-5.1.0.tgz", + "integrity": "sha512-FFOeqLR+Ng+iGoapZksAYwNFMSxTqQaFt32UHFrIDwa8bk72mWMWH5U/LEpvhnQh5ZD1sWZFbh3oTNBcFtt+4A==", + "license": "MIT", + "dependencies": { + "lodash.groupby": "^4.6.0", + "lodash.merge": "^4.6.2", + "openapi3-ts": "^3.0.0", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.14.0" + } + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -9863,6 +9881,12 @@ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "license": "MIT" }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9916,7 +9940,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.once": { @@ -10696,6 +10719,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi3-ts": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", + "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", + "license": "MIT", + "dependencies": { + "yaml": "^2.2.1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13690,6 +13722,18 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 2f6b5ae..7c6cc1f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "bullmq": "5.58.5", "class-transformer": "0.5.1", "class-validator": "0.14.1", + "class-validator-jsonschema": "5.1.0", "dotenv": "16.5.0", "ioredis": "5.7.0", "mongoose": "8.14.1", From 0ffb62429d7666f151738fc57c8816bc0614adfa Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:18:04 +0300 Subject: [PATCH 07/12] feat(mcp): add MCP tool response formatting and update authentication methods --- src/core/helper/index.ts | 1 + src/core/helper/mcp.helper.ts | 17 +++++++ src/modules/mcp/tools/auth.tools.ts | 74 +++++------------------------ 3 files changed, 31 insertions(+), 61 deletions(-) create mode 100644 src/core/helper/mcp.helper.ts diff --git a/src/core/helper/index.ts b/src/core/helper/index.ts index 5c57b21..f0e30ae 100644 --- a/src/core/helper/index.ts +++ b/src/core/helper/index.ts @@ -1,2 +1,3 @@ export * from './mongo.helper'; export * from './mask.helper'; +export * from './mcp.helper'; diff --git a/src/core/helper/mcp.helper.ts b/src/core/helper/mcp.helper.ts new file mode 100644 index 0000000..60cf706 --- /dev/null +++ b/src/core/helper/mcp.helper.ts @@ -0,0 +1,17 @@ +export const formatMcpToolResponse = ( + message: string, + data: Record, +) => { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message, + data, + }), + }, + ], + }; +}; diff --git a/src/modules/mcp/tools/auth.tools.ts b/src/modules/mcp/tools/auth.tools.ts index 558eeda..91bc6ff 100644 --- a/src/modules/mcp/tools/auth.tools.ts +++ b/src/modules/mcp/tools/auth.tools.ts @@ -4,6 +4,8 @@ import { SignInDto } from '../../auth/dto'; import { CreateUserDto } from '../../user/dto'; import { McpTool } from 'src/core/interface'; import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; +import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; +import { formatMcpToolResponse } from 'src/core/helper'; export class AuthTools { constructor( @@ -12,46 +14,18 @@ export class AuthTools { ) {} getToolDefinitions() { + const schemas = validationMetadatasToSchemas(); + return [ { name: McpTool.AUTH_REGISTER.name, description: McpTool.AUTH_REGISTER.description, - inputSchema: { - type: 'object', - properties: { - fullname: { - type: 'string', - description: 'User full name', - }, - nickname: { - type: 'string', - description: 'User nickname', - }, - password: { - type: 'string', - description: 'User password', - }, - }, - required: ['fullname', 'nickname', 'password'], - }, + inputSchema: schemas.CreateUserDto, }, { name: McpTool.AUTH_LOGIN.name, description: McpTool.AUTH_LOGIN.description, - inputSchema: { - type: 'object', - properties: { - nickname: { - type: 'string', - description: 'User nickname', - }, - password: { - type: 'string', - description: 'User password', - }, - }, - required: ['nickname', 'password'], - }, + inputSchema: schemas.SignInDto, }, ]; } @@ -69,38 +43,16 @@ export class AuthTools { private async register(args: CreateUserDto) { const result = await this.userService.create(args); - return { - content: [ - { - type: 'text', - text: JSON.stringify({ - success: true, - message: 'User registered successfully', - data: { - id: result.id, - }, - }), - }, - ], - }; + return formatMcpToolResponse('User registered successfully', { + userId: result.id, + }); } private async login(args: SignInDto) { const result = await this.authService.signIn(args); - return { - content: [ - { - type: 'text', - text: JSON.stringify({ - success: true, - message: 'Login successful', - data: { - accessToken: result.accessToken, - refreshToken: result.refreshToken, - }, - }), - }, - ], - }; + return formatMcpToolResponse('User logged in successfully', { + accessToken: result.accessToken, + refreshToken: result.refreshToken, + }); } } From ec2b0bdc5284bfc82215411312bb136df899091c Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:00:17 +0300 Subject: [PATCH 08/12] feat(mcp): enhance MCP tool with todo functionalities and update descriptions --- src/core/interface/mcp/mcp-tool.const.ts | 28 ++++---- src/modules/mcp/mcp.module.ts | 3 +- src/modules/mcp/server/mcp.server.ts | 27 +++++-- src/modules/mcp/tools/todo.tools.ts | 92 ++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 src/modules/mcp/tools/todo.tools.ts diff --git a/src/core/interface/mcp/mcp-tool.const.ts b/src/core/interface/mcp/mcp-tool.const.ts index c5eb7d3..d1e1578 100644 --- a/src/core/interface/mcp/mcp-tool.const.ts +++ b/src/core/interface/mcp/mcp-tool.const.ts @@ -1,45 +1,43 @@ export const McpTool = { AUTH_REGISTER: { name: 'auth_register', - description: 'Register a new user', + description: 'Register a new user. Start here to create your account.', category: 'auth' as const, }, AUTH_LOGIN: { name: 'auth_login', - description: 'Login user and get access token', + description: + 'Login user and get access token. Use this after registration to get your userId and tokens.', category: 'auth' as const, }, TODO_CREATE: { name: 'todo_create', - description: 'Create a new todo item', + description: + 'Create a new todo item. IMPORTANT: You must first login with auth_login to get your userId.', category: 'todo' as const, }, - TODO_LIST: { - name: 'todo_list', - description: 'List user todos with pagination', + TODO_GET: { + name: 'todo_get', + description: + 'Get user todos with pagination. IMPORTANT: You must provide the userId from auth_login response.', category: 'todo' as const, }, TODO_UPDATE: { name: 'todo_update', - description: 'Update an existing todo', + description: + 'Update an existing todo. IMPORTANT: You need both todoId (from todo_create/todo_get) and userId (from auth_login).', category: 'todo' as const, }, TODO_DELETE: { name: 'todo_delete', description: - 'Schedule todo for deletion (delayed 4 seconds) - returns pending status, user can cancel within this time', + 'Schedule todo for deletion (delayed 4 seconds). IMPORTANT: You need todoId and userId. Returns jobId for potential cancellation.', category: 'todo' as const, }, TODO_CANCEL_DELETION: { name: 'todo_cancel_deletion', description: - 'Cancel pending todo deletion and restore the todo (only works within 4 seconds of deletion request)', + 'Cancel pending todo deletion (only works within 4 seconds). IMPORTANT: Use the todoId and userId to identify which deletion to cancel.', category: 'todo' as const, }, } as const; - -// Type utilities -export type McpToolKey = keyof typeof McpTool; -export type McpToolValue = (typeof McpTool)[McpToolKey]; -export type McpToolName = McpToolValue['name']; -export type McpToolCategory = McpToolValue['category']; diff --git a/src/modules/mcp/mcp.module.ts b/src/modules/mcp/mcp.module.ts index f616678..14b15e6 100644 --- a/src/modules/mcp/mcp.module.ts +++ b/src/modules/mcp/mcp.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { McpServer } from './server/mcp.server'; import { AuthModule } from '../auth/auth.module'; import { UserModule } from '../user/user.module'; +import { TodoModule } from '../todo/todo.module'; @Module({ - imports: [AuthModule, UserModule], + imports: [AuthModule, UserModule, TodoModule], providers: [McpServer], exports: [McpServer], }) diff --git a/src/modules/mcp/server/mcp.server.ts b/src/modules/mcp/server/mcp.server.ts index 7824b52..c3f2685 100644 --- a/src/modules/mcp/server/mcp.server.ts +++ b/src/modules/mcp/server/mcp.server.ts @@ -11,17 +11,22 @@ import { UserService } from 'src/modules/user/service/user.service'; import { McpTool } from 'src/core/interface/mcp/mcp-tool.const'; import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; import { McpCategoryNotSupportedException } from 'src/core/error/exception/mcp-category-not-supported.exception'; +import { TodoTools } from '../tools/todo.tools'; +import { TodoService } from 'src/modules/todo/service/todo.service'; @Injectable() export class McpServer { private server: Server; private authTools: AuthTools; + private todoTools: TodoTools; constructor( private readonly authService: AuthService, private readonly userService: UserService, + private readonly todoService: TodoService, ) { this.authTools = new AuthTools(this.authService, this.userService); + this.todoTools = new TodoTools(this.todoService); this.initializeServer(); } @@ -44,7 +49,10 @@ export class McpServer { private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { - tools: [...this.authTools.getToolDefinitions()], + tools: [ + ...this.authTools.getToolDefinitions(), + ...this.todoTools.getToolDefinitions(), + ], }; }); @@ -53,18 +61,25 @@ export class McpServer { try { // Category-based routing - const allTools = Object.values(McpTool); + const allTools: Array<{ + name: string; + description: string; + category: string; + }> = Object.values(McpTool); const tool = allTools.find((t) => t.name === name); if (!tool) { throw new McpToolNotFoundException(name); } - if (tool.category === 'auth') { - return await this.authTools.handleToolCall(name, args); + switch (tool.category) { + case 'auth': + return await this.authTools.handleToolCall(name, args); + case 'todo': + return await this.todoTools.handleToolCall(name, args); + default: + throw new McpCategoryNotSupportedException(tool.category); } - - throw new McpCategoryNotSupportedException(tool.category); } catch (error) { return { content: [ diff --git a/src/modules/mcp/tools/todo.tools.ts b/src/modules/mcp/tools/todo.tools.ts new file mode 100644 index 0000000..2445f00 --- /dev/null +++ b/src/modules/mcp/tools/todo.tools.ts @@ -0,0 +1,92 @@ +import { McpTool } from 'src/core/interface'; +import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; +import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; +import { formatMcpToolResponse } from 'src/core/helper'; +import { TodoService } from 'src/modules/todo/service'; +import { UpdateTodoDto } from 'src/modules/todo/dto/update-todo.dto'; +import { GetTodoDto } from 'src/modules/todo/dto/get-todo.dto'; +import { CreateTodoDto } from 'src/modules/todo/dto/create-todo.dto'; + +export class TodoTools { + constructor(private readonly todoService: TodoService) {} + + getToolDefinitions() { + const schemas = validationMetadatasToSchemas(); + + return [ + { + name: McpTool.TODO_CREATE.name, + description: McpTool.TODO_CREATE.description, + inputSchema: schemas.CreateTodoDto, + }, + { + name: McpTool.TODO_GET.name, + description: McpTool.TODO_GET.description, + inputSchema: schemas.GetTodoDto, + }, + { + name: McpTool.TODO_UPDATE.name, + description: McpTool.TODO_UPDATE.description, + inputSchema: schemas.UpdateTodoDto, + }, + { + name: McpTool.TODO_DELETE.name, + description: McpTool.TODO_DELETE.description, + inputSchema: schemas.DeleteTodoDto, + }, + { + name: McpTool.TODO_CANCEL_DELETION.name, + description: McpTool.TODO_CANCEL_DELETION.description, + inputSchema: schemas.CancelDeletionDto, + }, + ]; + } + + async handleToolCall(toolName: string, args: any) { + switch (toolName) { + case McpTool.TODO_CREATE.name: + return await this.create(args); + case McpTool.TODO_GET.name: + return await this.get(args); + case McpTool.TODO_UPDATE.name: + return await this.update(args); + case McpTool.TODO_DELETE.name: + return await this.delete(args); + case McpTool.TODO_CANCEL_DELETION.name: + return await this.cancelDeletion(args); + default: + throw new McpToolNotFoundException(toolName); + } + } + + private async create(args: CreateTodoDto) { + const result = await this.todoService.create(args); + return formatMcpToolResponse('Todo created successfully', result); + } + + private async get(args: { userId: string } & GetTodoDto) { + const { userId, ...getTodoDto } = args; + const result = await this.todoService.findByUserId(userId, getTodoDto); + return formatMcpToolResponse('Todos retrieved successfully', result); + } + + private async update( + args: { todoId: string; userId: string } & UpdateTodoDto, + ) { + const { todoId, userId, ...updateTodoDto } = args; + const result = await this.todoService.update(todoId, userId, updateTodoDto); + return formatMcpToolResponse('Todo updated successfully', result); + } + + private async delete(args: { todoId: string; userId: string }) { + const { todoId, userId } = args; + const result = await this.todoService.delete({ todoId, userId }); + return formatMcpToolResponse('Todo deletion scheduled', result); + } + + private async cancelDeletion(args: { todoId: string; userId: string }) { + const { todoId, userId } = args; + const result = await this.todoService.cancelDeletion({ todoId, userId }); + return formatMcpToolResponse('Todo deletion cancelled', result); + } +} From 31106ce382761ed122dfdfc1ad0e1e34202338be Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:00:31 +0300 Subject: [PATCH 09/12] feat(todo): refactor todo service and DTOs to include userId in create, delete, and cancel deletion operations --- src/mcp-server.ts | 2 +- .../todo/controller/todo.controller.ts | 6 ++-- src/modules/todo/dto/create-todo.dto.ts | 4 +++ src/modules/todo/dto/delete-todo.dto.ts | 33 +++++++++++++++++++ src/modules/todo/service/todo.service.ts | 28 ++++++++-------- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 5ab9ea2..1e685f3 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -3,7 +3,7 @@ import { AppModule } from './app.module'; import { McpServer } from './modules/mcp/server/mcp.server'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { logger: false }); const mcpServer = app.get(McpServer); diff --git a/src/modules/todo/controller/todo.controller.ts b/src/modules/todo/controller/todo.controller.ts index ae5a1dc..39904ef 100644 --- a/src/modules/todo/controller/todo.controller.ts +++ b/src/modules/todo/controller/todo.controller.ts @@ -54,7 +54,7 @@ export class TodoController { @Body() createTodoDto: CreateTodoDto, @ReqUser() user: User, ): Promise { - return this.todoService.create(createTodoDto, user._id); + return this.todoService.create({ ...createTodoDto, userId: user._id }); } @Get() @@ -100,7 +100,7 @@ export class TodoController { @Param('todoId') todoId: string, @ReqUser() user: User, ): Promise { - return this.todoService.delete(todoId, user._id); + return this.todoService.delete({ todoId, userId: user._id }); } @Post(':todoId/cancel-deletion') @@ -114,7 +114,7 @@ export class TodoController { @Param('todoId') todoId: string, @ReqUser() user: User, ): Promise { - return this.todoService.cancelDeletion(todoId, user._id); + return this.todoService.cancelDeletion({ todoId, userId: user._id }); } @Get('search') diff --git a/src/modules/todo/dto/create-todo.dto.ts b/src/modules/todo/dto/create-todo.dto.ts index 0c24e3e..3d7aef0 100644 --- a/src/modules/todo/dto/create-todo.dto.ts +++ b/src/modules/todo/dto/create-todo.dto.ts @@ -15,6 +15,10 @@ export class CreateTodoDto { @IsString() @ApiProperty() description: string; + + @IsString() + @ApiProperty() + userId: string; } export class CreateTodoAck { diff --git a/src/modules/todo/dto/delete-todo.dto.ts b/src/modules/todo/dto/delete-todo.dto.ts index ce905d5..331624e 100644 --- a/src/modules/todo/dto/delete-todo.dto.ts +++ b/src/modules/todo/dto/delete-todo.dto.ts @@ -1,4 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class DeleteTodoDto { + @IsString() + @ApiProperty({ + description: 'Todo ID to delete', + example: '507f1f77bcf86cd799439012', + }) + todoId: string; + + @IsString() + @ApiProperty({ + description: 'User ID', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} export class DeleteTodoAck { @ApiProperty({ @@ -20,6 +37,22 @@ export class DeleteTodoAck { remainingTime: number; } +export class CancelDeletionDto { + @IsString() + @ApiProperty({ + description: 'Todo ID to cancel deletion', + example: '507f1f77bcf86cd799439012', + }) + todoId: string; + + @IsString() + @ApiProperty({ + description: 'User ID', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} + export class CancelDeletionAck { @ApiProperty({ description: 'Message about cancellation result', diff --git a/src/modules/todo/service/todo.service.ts b/src/modules/todo/service/todo.service.ts index e1aa510..6b890ec 100644 --- a/src/modules/todo/service/todo.service.ts +++ b/src/modules/todo/service/todo.service.ts @@ -9,6 +9,8 @@ import { UpdateTodoDto, DeleteTodoAck, CancelDeletionAck, + DeleteTodoDto, + CancelDeletionDto, } from '../dto'; import { Todo } from 'src/core/interface'; import { @@ -42,10 +44,9 @@ export class TodoService { ); } - async create(todo: CreateTodoDto, userId: string): Promise { + async create(todo: CreateTodoDto): Promise { const todoData = { ...todo, - userId, completed: false, // Default to false when creating a new todo }; @@ -123,16 +124,16 @@ export class TodoService { return updatedTodo; } - async delete(todoId: string, userId: string): Promise { - const lockKey = cacheKeys.locks.todoDeletion(todoId); + async delete(deleteTodoDto: DeleteTodoDto): Promise { + const lockKey = cacheKeys.locks.todoDeletion(deleteTodoDto.todoId); return await this.redlockService.withLock( lockKey, async () => { // 1. Pending deletion kontrolΓΌ const isPending = await this.todoDeletionJobService.isPendingDeletion( - todoId, - userId, + deleteTodoDto.todoId, + deleteTodoDto.userId, ); if (isPending) { throw new TodoDeletionPendingException(); @@ -140,8 +141,8 @@ export class TodoService { // 2. Check if todo exists const existingTodo = await this.todoRepository.findByIdAndUserId( - todoId, - userId, + deleteTodoDto.todoId, + deleteTodoDto.userId, ); if (!existingTodo) { throw new TodoNotFoundException(); @@ -149,8 +150,8 @@ export class TodoService { // 3. Schedule delayed job const jobId = await this.todoDeletionJobService.scheduleDeletion( - todoId, - userId, + deleteTodoDto.todoId, + deleteTodoDto.userId, ); return { @@ -164,12 +165,11 @@ export class TodoService { } async cancelDeletion( - todoId: string, - userId: string, + cancelDeletionDto: CancelDeletionDto, ): Promise { const cancelled = await this.todoDeletionJobService.cancelDeletion( - todoId, - userId, + cancelDeletionDto.todoId, + cancelDeletionDto.userId, ); return { From c56ea5120e17d465156aa15a2fbf9140d05e57fd Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:45:51 +0300 Subject: [PATCH 10/12] revert(todo): dto changes for mcp are reverted --- .../todo/controller/todo.controller.ts | 6 ++-- src/modules/todo/dto/create-todo.dto.ts | 4 --- src/modules/todo/dto/delete-todo.dto.ts | 32 ------------------- src/modules/todo/service/todo.service.ts | 28 ++++++++-------- 4 files changed, 17 insertions(+), 53 deletions(-) diff --git a/src/modules/todo/controller/todo.controller.ts b/src/modules/todo/controller/todo.controller.ts index 39904ef..ae5a1dc 100644 --- a/src/modules/todo/controller/todo.controller.ts +++ b/src/modules/todo/controller/todo.controller.ts @@ -54,7 +54,7 @@ export class TodoController { @Body() createTodoDto: CreateTodoDto, @ReqUser() user: User, ): Promise { - return this.todoService.create({ ...createTodoDto, userId: user._id }); + return this.todoService.create(createTodoDto, user._id); } @Get() @@ -100,7 +100,7 @@ export class TodoController { @Param('todoId') todoId: string, @ReqUser() user: User, ): Promise { - return this.todoService.delete({ todoId, userId: user._id }); + return this.todoService.delete(todoId, user._id); } @Post(':todoId/cancel-deletion') @@ -114,7 +114,7 @@ export class TodoController { @Param('todoId') todoId: string, @ReqUser() user: User, ): Promise { - return this.todoService.cancelDeletion({ todoId, userId: user._id }); + return this.todoService.cancelDeletion(todoId, user._id); } @Get('search') diff --git a/src/modules/todo/dto/create-todo.dto.ts b/src/modules/todo/dto/create-todo.dto.ts index 3d7aef0..0c24e3e 100644 --- a/src/modules/todo/dto/create-todo.dto.ts +++ b/src/modules/todo/dto/create-todo.dto.ts @@ -15,10 +15,6 @@ export class CreateTodoDto { @IsString() @ApiProperty() description: string; - - @IsString() - @ApiProperty() - userId: string; } export class CreateTodoAck { diff --git a/src/modules/todo/dto/delete-todo.dto.ts b/src/modules/todo/dto/delete-todo.dto.ts index 331624e..d88836b 100644 --- a/src/modules/todo/dto/delete-todo.dto.ts +++ b/src/modules/todo/dto/delete-todo.dto.ts @@ -1,22 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -export class DeleteTodoDto { - @IsString() - @ApiProperty({ - description: 'Todo ID to delete', - example: '507f1f77bcf86cd799439012', - }) - todoId: string; - - @IsString() - @ApiProperty({ - description: 'User ID', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} - export class DeleteTodoAck { @ApiProperty({ description: 'Success message about deletion scheduling', @@ -37,22 +21,6 @@ export class DeleteTodoAck { remainingTime: number; } -export class CancelDeletionDto { - @IsString() - @ApiProperty({ - description: 'Todo ID to cancel deletion', - example: '507f1f77bcf86cd799439012', - }) - todoId: string; - - @IsString() - @ApiProperty({ - description: 'User ID', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} - export class CancelDeletionAck { @ApiProperty({ description: 'Message about cancellation result', diff --git a/src/modules/todo/service/todo.service.ts b/src/modules/todo/service/todo.service.ts index 6b890ec..e1aa510 100644 --- a/src/modules/todo/service/todo.service.ts +++ b/src/modules/todo/service/todo.service.ts @@ -9,8 +9,6 @@ import { UpdateTodoDto, DeleteTodoAck, CancelDeletionAck, - DeleteTodoDto, - CancelDeletionDto, } from '../dto'; import { Todo } from 'src/core/interface'; import { @@ -44,9 +42,10 @@ export class TodoService { ); } - async create(todo: CreateTodoDto): Promise { + async create(todo: CreateTodoDto, userId: string): Promise { const todoData = { ...todo, + userId, completed: false, // Default to false when creating a new todo }; @@ -124,16 +123,16 @@ export class TodoService { return updatedTodo; } - async delete(deleteTodoDto: DeleteTodoDto): Promise { - const lockKey = cacheKeys.locks.todoDeletion(deleteTodoDto.todoId); + async delete(todoId: string, userId: string): Promise { + const lockKey = cacheKeys.locks.todoDeletion(todoId); return await this.redlockService.withLock( lockKey, async () => { // 1. Pending deletion kontrolΓΌ const isPending = await this.todoDeletionJobService.isPendingDeletion( - deleteTodoDto.todoId, - deleteTodoDto.userId, + todoId, + userId, ); if (isPending) { throw new TodoDeletionPendingException(); @@ -141,8 +140,8 @@ export class TodoService { // 2. Check if todo exists const existingTodo = await this.todoRepository.findByIdAndUserId( - deleteTodoDto.todoId, - deleteTodoDto.userId, + todoId, + userId, ); if (!existingTodo) { throw new TodoNotFoundException(); @@ -150,8 +149,8 @@ export class TodoService { // 3. Schedule delayed job const jobId = await this.todoDeletionJobService.scheduleDeletion( - deleteTodoDto.todoId, - deleteTodoDto.userId, + todoId, + userId, ); return { @@ -165,11 +164,12 @@ export class TodoService { } async cancelDeletion( - cancelDeletionDto: CancelDeletionDto, + todoId: string, + userId: string, ): Promise { const cancelled = await this.todoDeletionJobService.cancelDeletion( - cancelDeletionDto.todoId, - cancelDeletionDto.userId, + todoId, + userId, ); return { From 6dd660fed45b36b3c302092c88e101edee0fcef1 Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:28:03 +0300 Subject: [PATCH 11/12] feat(mcp): -NOT WORKED- add DTOs for todo operations including userId for create, delete, and cancel deletion --- src/modules/mcp/dto/index.ts | 1 + .../mcp/dto/todo/cancel-deletion.dto.ts | 18 +++++++ src/modules/mcp/dto/todo/create-todo.dto.ts | 12 +++++ src/modules/mcp/dto/todo/delete-todo.dto.ts | 18 +++++++ src/modules/mcp/dto/todo/get-todo.dto.ts | 12 +++++ src/modules/mcp/dto/todo/index.ts | 5 ++ src/modules/mcp/dto/todo/update-todo.dto.ts | 19 +++++++ src/modules/mcp/tools/todo.tools.ts | 49 ++++++++++--------- 8 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 src/modules/mcp/dto/index.ts create mode 100644 src/modules/mcp/dto/todo/cancel-deletion.dto.ts create mode 100644 src/modules/mcp/dto/todo/create-todo.dto.ts create mode 100644 src/modules/mcp/dto/todo/delete-todo.dto.ts create mode 100644 src/modules/mcp/dto/todo/get-todo.dto.ts create mode 100644 src/modules/mcp/dto/todo/index.ts create mode 100644 src/modules/mcp/dto/todo/update-todo.dto.ts diff --git a/src/modules/mcp/dto/index.ts b/src/modules/mcp/dto/index.ts new file mode 100644 index 0000000..2df5c37 --- /dev/null +++ b/src/modules/mcp/dto/index.ts @@ -0,0 +1 @@ +export * from './todo'; diff --git a/src/modules/mcp/dto/todo/cancel-deletion.dto.ts b/src/modules/mcp/dto/todo/cancel-deletion.dto.ts new file mode 100644 index 0000000..2da8673 --- /dev/null +++ b/src/modules/mcp/dto/todo/cancel-deletion.dto.ts @@ -0,0 +1,18 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class McpCancelDeletionDto { + @IsString() + @ApiProperty({ + description: 'REQUIRED: Todo ID to cancel deletion', + example: '507f1f77bcf86cd799439012', + }) + todoId: string; + + @IsString() + @ApiProperty({ + description: 'REQUIRED: Your user ID from auth_login response', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} diff --git a/src/modules/mcp/dto/todo/create-todo.dto.ts b/src/modules/mcp/dto/todo/create-todo.dto.ts new file mode 100644 index 0000000..c46ee30 --- /dev/null +++ b/src/modules/mcp/dto/todo/create-todo.dto.ts @@ -0,0 +1,12 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { CreateTodoDto } from '../../../todo/dto'; + +export class McpCreateTodoDto extends CreateTodoDto { + @IsString() + @ApiProperty({ + description: 'REQUIRED: Your user ID from auth_login response', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} diff --git a/src/modules/mcp/dto/todo/delete-todo.dto.ts b/src/modules/mcp/dto/todo/delete-todo.dto.ts new file mode 100644 index 0000000..8b650a9 --- /dev/null +++ b/src/modules/mcp/dto/todo/delete-todo.dto.ts @@ -0,0 +1,18 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class McpDeleteTodoDto { + @IsString() + @ApiProperty({ + description: 'REQUIRED: Todo ID to delete', + example: '507f1f77bcf86cd799439012', + }) + todoId: string; + + @IsString() + @ApiProperty({ + description: 'REQUIRED: Your user ID from auth_login response', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} diff --git a/src/modules/mcp/dto/todo/get-todo.dto.ts b/src/modules/mcp/dto/todo/get-todo.dto.ts new file mode 100644 index 0000000..f09ad2b --- /dev/null +++ b/src/modules/mcp/dto/todo/get-todo.dto.ts @@ -0,0 +1,12 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { GetTodoDto } from '../../../todo/dto'; + +export class McpGetTodoDto extends GetTodoDto { + @IsString() + @ApiProperty({ + description: 'REQUIRED: Your user ID from auth_login response', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} diff --git a/src/modules/mcp/dto/todo/index.ts b/src/modules/mcp/dto/todo/index.ts new file mode 100644 index 0000000..2e077c9 --- /dev/null +++ b/src/modules/mcp/dto/todo/index.ts @@ -0,0 +1,5 @@ +export * from './create-todo.dto'; +export * from './get-todo.dto'; +export * from './update-todo.dto'; +export * from './delete-todo.dto'; +export * from './cancel-deletion.dto'; diff --git a/src/modules/mcp/dto/todo/update-todo.dto.ts b/src/modules/mcp/dto/todo/update-todo.dto.ts new file mode 100644 index 0000000..3e495ba --- /dev/null +++ b/src/modules/mcp/dto/todo/update-todo.dto.ts @@ -0,0 +1,19 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { UpdateTodoDto } from '../../../todo/dto'; + +export class McpUpdateTodoDto extends UpdateTodoDto { + @IsString() + @ApiProperty({ + description: 'REQUIRED: Todo ID from todo_create or todo_get response', + example: '507f1f77bcf86cd799439012', + }) + todoId: string; + + @IsString() + @ApiProperty({ + description: 'REQUIRED: Your user ID from auth_login response', + example: '507f1f77bcf86cd799439011', + }) + userId: string; +} diff --git a/src/modules/mcp/tools/todo.tools.ts b/src/modules/mcp/tools/todo.tools.ts index 2445f00..f49413a 100644 --- a/src/modules/mcp/tools/todo.tools.ts +++ b/src/modules/mcp/tools/todo.tools.ts @@ -3,9 +3,13 @@ import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not- import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; import { formatMcpToolResponse } from 'src/core/helper'; import { TodoService } from 'src/modules/todo/service'; -import { UpdateTodoDto } from 'src/modules/todo/dto/update-todo.dto'; -import { GetTodoDto } from 'src/modules/todo/dto/get-todo.dto'; -import { CreateTodoDto } from 'src/modules/todo/dto/create-todo.dto'; +import { + McpCreateTodoDto, + McpGetTodoDto, + McpUpdateTodoDto, + McpDeleteTodoDto, + McpCancelDeletionDto, +} from '../dto'; export class TodoTools { constructor(private readonly todoService: TodoService) {} @@ -17,27 +21,27 @@ export class TodoTools { { name: McpTool.TODO_CREATE.name, description: McpTool.TODO_CREATE.description, - inputSchema: schemas.CreateTodoDto, + inputSchema: schemas.McpCreateTodoDto, }, { name: McpTool.TODO_GET.name, description: McpTool.TODO_GET.description, - inputSchema: schemas.GetTodoDto, + inputSchema: schemas.McpGetTodoDto, }, { name: McpTool.TODO_UPDATE.name, description: McpTool.TODO_UPDATE.description, - inputSchema: schemas.UpdateTodoDto, + inputSchema: schemas.McpUpdateTodoDto, }, { name: McpTool.TODO_DELETE.name, description: McpTool.TODO_DELETE.description, - inputSchema: schemas.DeleteTodoDto, + inputSchema: schemas.McpDeleteTodoDto, }, { name: McpTool.TODO_CANCEL_DELETION.name, description: McpTool.TODO_CANCEL_DELETION.description, - inputSchema: schemas.CancelDeletionDto, + inputSchema: schemas.McpCancelDeletionDto, }, ]; } @@ -45,48 +49,47 @@ export class TodoTools { async handleToolCall(toolName: string, args: any) { switch (toolName) { case McpTool.TODO_CREATE.name: - return await this.create(args); + return await this.create(args as McpCreateTodoDto); case McpTool.TODO_GET.name: - return await this.get(args); + return await this.get(args as McpGetTodoDto); case McpTool.TODO_UPDATE.name: - return await this.update(args); + return await this.update(args as McpUpdateTodoDto); case McpTool.TODO_DELETE.name: - return await this.delete(args); + return await this.delete(args as McpDeleteTodoDto); case McpTool.TODO_CANCEL_DELETION.name: - return await this.cancelDeletion(args); + return await this.cancelDeletion(args as McpCancelDeletionDto); default: throw new McpToolNotFoundException(toolName); } } - private async create(args: CreateTodoDto) { - const result = await this.todoService.create(args); + private async create(args: McpCreateTodoDto) { + const { userId, ...createTodoDto } = args; + const result = await this.todoService.create(createTodoDto, userId); return formatMcpToolResponse('Todo created successfully', result); } - private async get(args: { userId: string } & GetTodoDto) { + private async get(args: McpGetTodoDto) { const { userId, ...getTodoDto } = args; const result = await this.todoService.findByUserId(userId, getTodoDto); return formatMcpToolResponse('Todos retrieved successfully', result); } - private async update( - args: { todoId: string; userId: string } & UpdateTodoDto, - ) { + private async update(args: McpUpdateTodoDto) { const { todoId, userId, ...updateTodoDto } = args; const result = await this.todoService.update(todoId, userId, updateTodoDto); return formatMcpToolResponse('Todo updated successfully', result); } - private async delete(args: { todoId: string; userId: string }) { + private async delete(args: McpDeleteTodoDto) { const { todoId, userId } = args; - const result = await this.todoService.delete({ todoId, userId }); + const result = await this.todoService.delete(todoId, userId); return formatMcpToolResponse('Todo deletion scheduled', result); } - private async cancelDeletion(args: { todoId: string; userId: string }) { + private async cancelDeletion(args: McpCancelDeletionDto) { const { todoId, userId } = args; - const result = await this.todoService.cancelDeletion({ todoId, userId }); + const result = await this.todoService.cancelDeletion(todoId, userId); return formatMcpToolResponse('Todo deletion cancelled', result); } } From 10137a0d09edcb674dbeb879d65c65218c726493 Mon Sep 17 00:00:00 2001 From: Ali Akkaya <117384310+alakkaya@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:49:16 +0300 Subject: [PATCH 12/12] feat(mcp): refactor MCP tool to use new schemas for authentication and todo operations, remove unused DTOs --- docker/docker-compose.yml | 2 +- package-lock.json | 46 +---------- package.json | 3 +- src/modules/mcp/dto/index.ts | 1 - .../mcp/dto/todo/cancel-deletion.dto.ts | 18 ---- src/modules/mcp/dto/todo/create-todo.dto.ts | 12 --- src/modules/mcp/dto/todo/delete-todo.dto.ts | 18 ---- src/modules/mcp/dto/todo/get-todo.dto.ts | 12 --- src/modules/mcp/dto/todo/index.ts | 5 -- src/modules/mcp/dto/todo/update-todo.dto.ts | 19 ----- src/modules/mcp/schemas/auth.schemas.ts | 34 ++++++++ src/modules/mcp/schemas/index.ts | 2 + src/modules/mcp/schemas/todo.schemas.ts | 47 +++++++++++ src/modules/mcp/tools/auth.tools.ts | 27 +++--- src/modules/mcp/tools/todo.tools.ts | 82 +++++++++++-------- 15 files changed, 148 insertions(+), 180 deletions(-) delete mode 100644 src/modules/mcp/dto/index.ts delete mode 100644 src/modules/mcp/dto/todo/cancel-deletion.dto.ts delete mode 100644 src/modules/mcp/dto/todo/create-todo.dto.ts delete mode 100644 src/modules/mcp/dto/todo/delete-todo.dto.ts delete mode 100644 src/modules/mcp/dto/todo/get-todo.dto.ts delete mode 100644 src/modules/mcp/dto/todo/index.ts delete mode 100644 src/modules/mcp/dto/todo/update-todo.dto.ts create mode 100644 src/modules/mcp/schemas/auth.schemas.ts create mode 100644 src/modules/mcp/schemas/index.ts create mode 100644 src/modules/mcp/schemas/todo.schemas.ts diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9b00337..65d06eb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ services: build: context: .. dockerfile: ./docker/Dockerfile - command: ['npm', 'run', 'mcp:dev'] + command: npm run mcp:dev volumes: - ../src:/usr/src/app/src depends_on: diff --git a/package-lock.json b/package-lock.json index 5b5d02c..cfe2781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "bullmq": "5.58.5", "class-transformer": "0.5.1", "class-validator": "0.14.1", - "class-validator-jsonschema": "^5.1.0", "dotenv": "16.5.0", "ioredis": "5.7.0", "mongoose": "8.14.1", @@ -6291,23 +6290,6 @@ "validator": "^13.9.0" } }, - "node_modules/class-validator-jsonschema": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/class-validator-jsonschema/-/class-validator-jsonschema-5.1.0.tgz", - "integrity": "sha512-FFOeqLR+Ng+iGoapZksAYwNFMSxTqQaFt32UHFrIDwa8bk72mWMWH5U/LEpvhnQh5ZD1sWZFbh3oTNBcFtt+4A==", - "license": "MIT", - "dependencies": { - "lodash.groupby": "^4.6.0", - "lodash.merge": "^4.6.2", - "openapi3-ts": "^3.0.0", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1" - }, - "peerDependencies": { - "class-transformer": "^0.4.0 || ^0.5.0", - "class-validator": "^0.14.0" - } - }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -9881,12 +9863,6 @@ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "license": "MIT" }, - "node_modules/lodash.groupby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", - "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9940,6 +9916,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, "license": "MIT" }, "node_modules/lodash.once": { @@ -10719,15 +10696,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi3-ts": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", - "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", - "license": "MIT", - "dependencies": { - "yaml": "^2.2.1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13722,18 +13690,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 7c6cc1f..815a391 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "mcp:dev": "ts-node -r tsconfig-paths/register src/mcp-server.ts", + "mcp:dev": "ts-node --transpile-only -r tsconfig-paths/register src/mcp-server.ts", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", @@ -39,7 +39,6 @@ "bullmq": "5.58.5", "class-transformer": "0.5.1", "class-validator": "0.14.1", - "class-validator-jsonschema": "5.1.0", "dotenv": "16.5.0", "ioredis": "5.7.0", "mongoose": "8.14.1", diff --git a/src/modules/mcp/dto/index.ts b/src/modules/mcp/dto/index.ts deleted file mode 100644 index 2df5c37..0000000 --- a/src/modules/mcp/dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './todo'; diff --git a/src/modules/mcp/dto/todo/cancel-deletion.dto.ts b/src/modules/mcp/dto/todo/cancel-deletion.dto.ts deleted file mode 100644 index 2da8673..0000000 --- a/src/modules/mcp/dto/todo/cancel-deletion.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class McpCancelDeletionDto { - @IsString() - @ApiProperty({ - description: 'REQUIRED: Todo ID to cancel deletion', - example: '507f1f77bcf86cd799439012', - }) - todoId: string; - - @IsString() - @ApiProperty({ - description: 'REQUIRED: Your user ID from auth_login response', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} diff --git a/src/modules/mcp/dto/todo/create-todo.dto.ts b/src/modules/mcp/dto/todo/create-todo.dto.ts deleted file mode 100644 index c46ee30..0000000 --- a/src/modules/mcp/dto/todo/create-todo.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { CreateTodoDto } from '../../../todo/dto'; - -export class McpCreateTodoDto extends CreateTodoDto { - @IsString() - @ApiProperty({ - description: 'REQUIRED: Your user ID from auth_login response', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} diff --git a/src/modules/mcp/dto/todo/delete-todo.dto.ts b/src/modules/mcp/dto/todo/delete-todo.dto.ts deleted file mode 100644 index 8b650a9..0000000 --- a/src/modules/mcp/dto/todo/delete-todo.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class McpDeleteTodoDto { - @IsString() - @ApiProperty({ - description: 'REQUIRED: Todo ID to delete', - example: '507f1f77bcf86cd799439012', - }) - todoId: string; - - @IsString() - @ApiProperty({ - description: 'REQUIRED: Your user ID from auth_login response', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} diff --git a/src/modules/mcp/dto/todo/get-todo.dto.ts b/src/modules/mcp/dto/todo/get-todo.dto.ts deleted file mode 100644 index f09ad2b..0000000 --- a/src/modules/mcp/dto/todo/get-todo.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { GetTodoDto } from '../../../todo/dto'; - -export class McpGetTodoDto extends GetTodoDto { - @IsString() - @ApiProperty({ - description: 'REQUIRED: Your user ID from auth_login response', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} diff --git a/src/modules/mcp/dto/todo/index.ts b/src/modules/mcp/dto/todo/index.ts deleted file mode 100644 index 2e077c9..0000000 --- a/src/modules/mcp/dto/todo/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './create-todo.dto'; -export * from './get-todo.dto'; -export * from './update-todo.dto'; -export * from './delete-todo.dto'; -export * from './cancel-deletion.dto'; diff --git a/src/modules/mcp/dto/todo/update-todo.dto.ts b/src/modules/mcp/dto/todo/update-todo.dto.ts deleted file mode 100644 index 3e495ba..0000000 --- a/src/modules/mcp/dto/todo/update-todo.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { UpdateTodoDto } from '../../../todo/dto'; - -export class McpUpdateTodoDto extends UpdateTodoDto { - @IsString() - @ApiProperty({ - description: 'REQUIRED: Todo ID from todo_create or todo_get response', - example: '507f1f77bcf86cd799439012', - }) - todoId: string; - - @IsString() - @ApiProperty({ - description: 'REQUIRED: Your user ID from auth_login response', - example: '507f1f77bcf86cd799439011', - }) - userId: string; -} diff --git a/src/modules/mcp/schemas/auth.schemas.ts b/src/modules/mcp/schemas/auth.schemas.ts new file mode 100644 index 0000000..8df5113 --- /dev/null +++ b/src/modules/mcp/schemas/auth.schemas.ts @@ -0,0 +1,34 @@ +export const AuthSchemas = { + register: { + type: 'object', + properties: { + fullname: { + type: 'string', + description: "User's full name", + }, + nickname: { + type: 'string', + description: 'Unique username', + }, + password: { + type: 'string', + description: 'User password', + }, + }, + required: ['fullname', 'nickname', 'password'], + }, + login: { + type: 'object', + properties: { + nickname: { + type: 'string', + description: 'Username', + }, + password: { + type: 'string', + description: 'User password', + }, + }, + required: ['nickname', 'password'], + }, +}; diff --git a/src/modules/mcp/schemas/index.ts b/src/modules/mcp/schemas/index.ts new file mode 100644 index 0000000..42c68ea --- /dev/null +++ b/src/modules/mcp/schemas/index.ts @@ -0,0 +1,2 @@ +export * from './auth.schemas'; +export * from './todo.schemas'; diff --git a/src/modules/mcp/schemas/todo.schemas.ts b/src/modules/mcp/schemas/todo.schemas.ts new file mode 100644 index 0000000..162d6b8 --- /dev/null +++ b/src/modules/mcp/schemas/todo.schemas.ts @@ -0,0 +1,47 @@ +export const TodoSchemas = { + create: { + type: 'object', + properties: { + userId: { type: 'string', description: 'User ID from login' }, + title: { type: 'string', description: 'Todo title' }, + description: { type: 'string', description: 'Todo description' }, + }, + required: ['userId', 'title'], + }, + get: { + type: 'object', + properties: { + userId: { type: 'string', description: 'User ID' }, + page: { type: 'number', description: 'Page number' }, + limit: { type: 'number', description: 'Items per page' }, + }, + required: ['userId'], + }, + update: { + type: 'object', + properties: { + userId: { type: 'string' }, + todoId: { type: 'string' }, + title: { type: 'string' }, + description: { type: 'string' }, + completed: { type: 'boolean' }, + }, + required: ['userId', 'todoId'], + }, + delete: { + type: 'object', + properties: { + userId: { type: 'string' }, + todoId: { type: 'string' }, + }, + required: ['userId', 'todoId'], + }, + cancelDeletion: { + type: 'object', + properties: { + userId: { type: 'string' }, + todoId: { type: 'string' }, + }, + required: ['userId', 'todoId'], + }, +}; diff --git a/src/modules/mcp/tools/auth.tools.ts b/src/modules/mcp/tools/auth.tools.ts index 91bc6ff..abb75f0 100644 --- a/src/modules/mcp/tools/auth.tools.ts +++ b/src/modules/mcp/tools/auth.tools.ts @@ -1,11 +1,9 @@ import { AuthService } from '../../auth/service/auth.service'; import { UserService } from '../../user/service/user.service'; -import { SignInDto } from '../../auth/dto'; -import { CreateUserDto } from '../../user/dto'; import { McpTool } from 'src/core/interface'; import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; -import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; import { formatMcpToolResponse } from 'src/core/helper'; +import { AuthSchemas } from '../schemas'; export class AuthTools { constructor( @@ -14,18 +12,16 @@ export class AuthTools { ) {} getToolDefinitions() { - const schemas = validationMetadatasToSchemas(); - return [ { name: McpTool.AUTH_REGISTER.name, description: McpTool.AUTH_REGISTER.description, - inputSchema: schemas.CreateUserDto, + inputSchema: AuthSchemas.register, }, { name: McpTool.AUTH_LOGIN.name, description: McpTool.AUTH_LOGIN.description, - inputSchema: schemas.SignInDto, + inputSchema: AuthSchemas.login, }, ]; } @@ -41,15 +37,24 @@ export class AuthTools { } } - private async register(args: CreateUserDto) { - const result = await this.userService.create(args); + private async register(args: any) { + const result = await this.userService.create({ + fullname: args.fullname, + nickname: args.nickname, + password: args.password, + }); + return formatMcpToolResponse('User registered successfully', { userId: result.id, }); } - private async login(args: SignInDto) { - const result = await this.authService.signIn(args); + private async login(args: any) { + const result = await this.authService.signIn({ + nickname: args.nickname, + password: args.password, + }); + return formatMcpToolResponse('User logged in successfully', { accessToken: result.accessToken, refreshToken: result.refreshToken, diff --git a/src/modules/mcp/tools/todo.tools.ts b/src/modules/mcp/tools/todo.tools.ts index f49413a..84bbaf0 100644 --- a/src/modules/mcp/tools/todo.tools.ts +++ b/src/modules/mcp/tools/todo.tools.ts @@ -1,95 +1,105 @@ import { McpTool } from 'src/core/interface'; import { McpToolNotFoundException } from 'src/core/error/exception/mcp-tool-not-found.exception'; -import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; import { formatMcpToolResponse } from 'src/core/helper'; import { TodoService } from 'src/modules/todo/service'; -import { - McpCreateTodoDto, - McpGetTodoDto, - McpUpdateTodoDto, - McpDeleteTodoDto, - McpCancelDeletionDto, -} from '../dto'; +import { TodoSchemas } from '../schemas'; export class TodoTools { constructor(private readonly todoService: TodoService) {} getToolDefinitions() { - const schemas = validationMetadatasToSchemas(); - return [ { name: McpTool.TODO_CREATE.name, description: McpTool.TODO_CREATE.description, - inputSchema: schemas.McpCreateTodoDto, + inputSchema: TodoSchemas.create, }, { name: McpTool.TODO_GET.name, description: McpTool.TODO_GET.description, - inputSchema: schemas.McpGetTodoDto, + inputSchema: TodoSchemas.get, }, { name: McpTool.TODO_UPDATE.name, description: McpTool.TODO_UPDATE.description, - inputSchema: schemas.McpUpdateTodoDto, + inputSchema: TodoSchemas.update, }, { name: McpTool.TODO_DELETE.name, description: McpTool.TODO_DELETE.description, - inputSchema: schemas.McpDeleteTodoDto, + inputSchema: TodoSchemas.delete, }, { name: McpTool.TODO_CANCEL_DELETION.name, description: McpTool.TODO_CANCEL_DELETION.description, - inputSchema: schemas.McpCancelDeletionDto, + inputSchema: TodoSchemas.cancelDeletion, }, - ]; + ] as const; } async handleToolCall(toolName: string, args: any) { switch (toolName) { case McpTool.TODO_CREATE.name: - return await this.create(args as McpCreateTodoDto); + return await this.create(args); case McpTool.TODO_GET.name: - return await this.get(args as McpGetTodoDto); + return await this.get(args); case McpTool.TODO_UPDATE.name: - return await this.update(args as McpUpdateTodoDto); + return await this.update(args); case McpTool.TODO_DELETE.name: - return await this.delete(args as McpDeleteTodoDto); + return await this.delete(args); case McpTool.TODO_CANCEL_DELETION.name: - return await this.cancelDeletion(args as McpCancelDeletionDto); + return await this.cancelDeletion(args); default: throw new McpToolNotFoundException(toolName); } } - private async create(args: McpCreateTodoDto) { - const { userId, ...createTodoDto } = args; - const result = await this.todoService.create(createTodoDto, userId); + private async create(args: any) { + const createTodoDto = { + title: args.title, + description: args.description, + }; + + const result = await this.todoService.create(createTodoDto, args.userId); return formatMcpToolResponse('Todo created successfully', result); } - private async get(args: McpGetTodoDto) { - const { userId, ...getTodoDto } = args; - const result = await this.todoService.findByUserId(userId, getTodoDto); + private async get(args: any) { + const getTodoDto = { + page: args.page, + limit: args.limit, + completed: args.completed, + }; + + const result = await this.todoService.findByUserId(args.userId, getTodoDto); return formatMcpToolResponse('Todos retrieved successfully', result); } - private async update(args: McpUpdateTodoDto) { - const { todoId, userId, ...updateTodoDto } = args; - const result = await this.todoService.update(todoId, userId, updateTodoDto); + private async update(args: any) { + const updateTodoDto = { + title: args.title, + description: args.description, + completed: args.completed, + }; + + const result = await this.todoService.update( + args.todoId, + args.userId, + updateTodoDto, + ); return formatMcpToolResponse('Todo updated successfully', result); } - private async delete(args: McpDeleteTodoDto) { - const { todoId, userId } = args; - const result = await this.todoService.delete(todoId, userId); + private async delete(args: any) { + const result = await this.todoService.delete(args.todoId, args.userId); return formatMcpToolResponse('Todo deletion scheduled', result); } - private async cancelDeletion(args: McpCancelDeletionDto) { - const { todoId, userId } = args; - const result = await this.todoService.cancelDeletion(todoId, userId); + private async cancelDeletion(args: any) { + const result = await this.todoService.cancelDeletion( + args.todoId, + args.userId, + ); return formatMcpToolResponse('Todo deletion cancelled', result); } }