diff --git a/package.json b/package.json index db9cff2d..9285e695 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.5.13", + "version": "1.5.14", "main": "index.ts", "license": "BUSL-1.1", "scripts": { @@ -43,6 +43,8 @@ "@graphql-tools/utils": "^8.9.0", "@hawk.so/nodejs": "^3.3.2", "@hawk.so/types": "^0.5.9", + "@modelcontextprotocol/node": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "@n1ru4l/json-patch-plus": "^0.2.0", "@node-saml/node-saml": "^5.0.1", "@octokit/oauth-methods": "^4.0.0", diff --git a/src/index.ts b/src/index.ts index cb6f8d93..56984e2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ import ReleasesFactory from './models/releasesFactory'; import RedisHelper from './redisHelper'; import { appendSsoRoutes } from './sso'; import { appendGitHubRoutes } from './integrations/github'; +import { appendMCPRoutes } from './integrations/mcp'; /** * Option to enable playground @@ -272,6 +273,12 @@ class HawkAPI { */ appendGitHubRoutes(this.app, sharedFactories); + /** + * Append MCP integration routes to Express app using shared factories + * Note: This must be called after database connections are established + */ + appendMCPRoutes(this.app, sharedFactories); + await this.server.start(); this.app.use(graphqlUploadExpress()); this.server.applyMiddleware({ app: this.app }); diff --git a/src/integrations/mcp/index.ts b/src/integrations/mcp/index.ts new file mode 100644 index 00000000..e7bc2da5 --- /dev/null +++ b/src/integrations/mcp/index.ts @@ -0,0 +1,15 @@ +import express from 'express'; +import { ContextFactories } from 'src/types/graphql'; +import { createMCPRouter } from "./mcp"; + +/** + * Append MCP route to Express App + * + * @param app - Express application instance + * @param factories - context factories for database access + */ +export function appendMCPRoutes(app: express.Application, factories: ContextFactories): void { + const router = createMCPRouter(factories); + + app.use('/integration/mcp', router); +} diff --git a/src/integrations/mcp/mcp.ts b/src/integrations/mcp/mcp.ts new file mode 100644 index 00000000..7fcc5386 --- /dev/null +++ b/src/integrations/mcp/mcp.ts @@ -0,0 +1,68 @@ +import express from "express"; +import { McpServer } from '@modelcontextprotocol/server'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { ContextFactories } from 'src/types/graphql'; + +/** + * Create MCP router + * + * @param factories - context factories for database access + * @returns Express router with MCP integration endpoints + */ +export const createMCPRouter = (factories: ContextFactories): express.Router => { + const router = express.Router(); + const server = createMCPServer(factories); + + /** + * POST /integration/mcp + * Initiate MCP integration connection + */ + router.post("/", async (req, res, next) => { + try { + /** + * Each request must use its own transport + */ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + + await transport.handleRequest(req, res, req.body); + } catch (error) { + next(error); + } + }); + + return router; +}; + +/** + * Create MCP server + * + * @param factories - context factories for database access + * @returns configured MCP server + */ +const createMCPServer = (factories: ContextFactories) => { + const server = new McpServer({ + name: "server", + version: "0.0.0", + description: "A test server" + }); + + server.registerTool( + "hello_world", + { + description: "A test tool to greet the user" + }, + async () => { + return { + content: [ + { + type: "text" as const, + text: "Hi" + } + ] + } + } + ); + + return server; +}; diff --git a/yarn.lock b/yarn.lock index ff56e295..84e51237 100644 --- a/yarn.lock +++ b/yarn.lock @@ -517,6 +517,11 @@ dependencies: bson "^7.0.0" +"@hono/node-server@^1.19.9": + version "1.19.17" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.17.tgz#99bd8625cdbae82652a135c71363cd6322fae483" + integrity sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -855,6 +860,28 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@modelcontextprotocol/core@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/core/-/core-2.0.0.tgz#c918a4c6aef22a7c59dfb0aabc2a82c187cce157" + integrity sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA== + dependencies: + zod "^4.2.0" + +"@modelcontextprotocol/node@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/node/-/node-2.0.0.tgz#3851275cb1c08aeb113d5b7cbe6dbe7adab37c65" + integrity sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg== + dependencies: + "@hono/node-server" "^1.19.9" + +"@modelcontextprotocol/server@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/server/-/server-2.0.0.tgz#4fae5ccb8f7925ed9a6bf6595bb3b58617063ffd" + integrity sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw== + dependencies: + "@modelcontextprotocol/core" "2.0.0" + zod "^4.2.0" + "@mongodb-js/saslprep@^1.3.0": version "1.4.4" resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz#34a946ff6ae142e8f2259b87f2935f8284ba874d" @@ -7138,3 +7165,8 @@ zod@^3.25.76: version "3.25.76" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +zod@^4.2.0: + version "4.4.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" + integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==