From f41ebe4cf738516aba6d1033a6c0aca389ec6004 Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Fri, 6 Mar 2026 06:56:51 +0530 Subject: [PATCH 01/12] init mcp --- package.json | 4 +- pnpm-lock.yaml | 387 ++++++++++++++++++++++++++++++++++++++++++- server/corsair.ts | 4 +- server/mcp-server.ts | 151 +++++++++++++++++ 4 files changed, 534 insertions(+), 12 deletions(-) create mode 100644 server/mcp-server.ts diff --git a/package.json b/package.json index 2328584..8a81908 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "script": "tsx server/script.ts", + "mcp": "tsx server/mcp-server.ts", "new-plugin": "tsx scripts/new-plugin.ts", "format": "biome format . --write", "lint": "biome check .", @@ -24,10 +25,11 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.52", + "@modelcontextprotocol/sdk": "^1.27.1", "@trpc/server": "^11.10.0", "@whiskeysockets/baileys": "^7.0.0-rc.9", "chokidar": "^4.0.3", - "corsair": "0.1.13", + "corsair": "0.1.26", "dotenv": "^16.4.0", "drizzle-orm": "^0.44.5", "express": "^4.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10c4f27..48cd842 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.52 version: 0.2.52(zod@3.25.76) + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@trpc/server': specifier: ^11.10.0 version: 11.10.0(typescript@5.9.3) @@ -21,8 +24,8 @@ importers: specifier: ^4.0.3 version: 4.0.3 corsair: - specifier: 0.1.13 - version: 0.1.13 + specifier: 0.1.26 + version: 0.1.26 dotenv: specifier: ^16.4.0 version: 16.6.1 @@ -37,7 +40,7 @@ importers: version: 1.40.0(encoding@0.1.13) mem0ai: specifier: ^2.2.3 - version: 2.2.3(@anthropic-ai/sdk@0.55.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0)(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0) + version: 2.2.3(@anthropic-ai/sdk@0.55.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)))(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0) node-cron: specifier: ^4.2.1 version: 4.2.1 @@ -710,6 +713,12 @@ packages: '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -882,6 +891,16 @@ packages: '@mistralai/mistralai@1.14.0': resolution: {integrity: sha512-6zaj2f2LCd37cRpBvCgctkDbXtYBlAC85p+u4uU/726zjtsI+sdVH34qRzkm9iE3tRb8BoaiI0/P7TD+uMvLLQ==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@npmcli/fs@1.1.1': resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} @@ -1301,6 +1320,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -1317,6 +1340,17 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1391,6 +1425,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1514,6 +1552,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -1521,12 +1563,20 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - corsair@0.1.13: - resolution: {integrity: sha512-BcTK4DuPzNXbhYZ3Qu6b1BIH0dO1+VTScz2m/1BQs3Et35IIQv9uMJhI24Ij07kN4IPJ7YDnS37kFkET4fry9Q==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + corsair@0.1.26: + resolution: {integrity: sha512-ZuXfNgEfAl3luToN22OATIfL1OQ+ZTAmTK1eLi9tACLR0Xol3fd4OuXWhlMzQ6vsxE9zTVMQjjob6vZjOlv7og==} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -1816,6 +1866,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1828,13 +1886,29 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1863,6 +1937,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -1899,6 +1977,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2006,6 +2088,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + engines: {node: '>=16.9.0'} + hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -2047,6 +2133,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2071,6 +2161,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -2103,6 +2197,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -2133,6 +2230,9 @@ packages: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tiktoken@1.0.21: resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} @@ -2145,6 +2245,12 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -2269,6 +2375,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -2281,10 +2391,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -2378,6 +2496,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo4j-driver-bolt-connection@5.28.3: resolution: {integrity: sha512-wqHBYcU0FVRDmdsoZ+Fk0S/InYmu9/4BT6fPYh45Jimg/J7vQBUcdkiHGU7nop7HRb1ZgJmL305mJb6g5Bv35Q==} @@ -2431,6 +2553,10 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2515,6 +2641,9 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2577,6 +2706,10 @@ packages: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2659,6 +2792,10 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -2681,6 +2818,10 @@ packages: redis@4.7.1: resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2706,6 +2847,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -2732,10 +2877,18 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -2945,6 +3098,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -3586,12 +3743,14 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@google/genai@1.42.0': + '@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76))': dependencies: google-auth-library: 10.5.0 p-retry: 4.6.2 protobufjs: 7.5.4 ws: 8.19.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) transitivePeerDependencies: - bufferutil - supports-color @@ -3605,6 +3764,10 @@ snapshots: '@hapi/hoek@9.3.0': {} + '@hono/node-server@1.19.11(hono@4.12.5)': + dependencies: + hono: 4.12.5 + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -3766,6 +3929,30 @@ snapshots: - bufferutil - utf-8-validate + '@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.12.5 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 @@ -4177,6 +4364,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -4196,6 +4388,17 @@ snapshots: indent-string: 4.0.0 optional: true + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -4275,6 +4478,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4426,13 +4643,22 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} - corsair@0.1.13: + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + corsair@0.1.26: dependencies: kysely: 0.28.11 uuid: 13.0.0 @@ -4675,6 +4901,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -4687,6 +4919,11 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + express@4.22.1: dependencies: accepts: 1.3.8 @@ -4723,8 +4960,45 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -4761,6 +5035,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + follow-redirects@1.15.11: {} foreground-child@3.3.1: @@ -4791,6 +5076,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-minipass@2.1.0: @@ -4947,6 +5234,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.12.5: {} + hookified@1.15.1: {} http-cache-semantics@4.2.0: @@ -5006,6 +5295,10 @@ snapshots: safer-buffer: 2.1.2 optional: true + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} imurmurhash@0.1.4: @@ -5027,6 +5320,8 @@ snapshots: ini@1.3.8: {} + ip-address@10.0.1: {} + ip-address@10.1.0: optional: true @@ -5047,6 +5342,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -5096,6 +5393,8 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.1 + jose@6.1.3: {} + js-tiktoken@1.0.21: dependencies: base64-js: 1.5.1 @@ -5108,6 +5407,10 @@ snapshots: dependencies: bignumber.js: 9.3.1 + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -5218,13 +5521,13 @@ snapshots: media-typer@1.1.0: {} - mem0ai@2.2.3(@anthropic-ai/sdk@0.55.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0)(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0): + mem0ai@2.2.3(@anthropic-ai/sdk@0.55.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)))(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0): dependencies: '@anthropic-ai/sdk': 0.55.1 '@azure/identity': 4.13.0 '@azure/search-documents': 12.2.0 '@cloudflare/workers-types': 4.20260302.0 - '@google/genai': 1.42.0 + '@google/genai': 1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)) '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)) '@mistralai/mistralai': 1.14.0 '@qdrant/js-client-rest': 1.13.0(typescript@5.9.3) @@ -5250,6 +5553,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + methods@1.1.2: {} micromatch@4.0.8: @@ -5259,10 +5564,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-response@3.1.0: {} @@ -5354,6 +5665,8 @@ snapshots: negotiator@0.6.4: optional: true + negotiator@1.0.0: {} + neo4j-driver-bolt-connection@5.28.3: dependencies: buffer: 6.0.3 @@ -5420,6 +5733,8 @@ snapshots: set-blocking: 2.0.0 optional: true + object-assign@4.1.1: {} + object-inspect@1.13.4: {} ollama@0.5.18: @@ -5502,6 +5817,8 @@ snapshots: path-to-regexp@0.1.12: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -5567,6 +5884,8 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 3.1.0 + pkce-challenge@5.0.1: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5677,6 +5996,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -5705,6 +6031,8 @@ snapshots: '@redis/search': 1.2.0(@redis/client@1.6.1) '@redis/time-series': 1.1.0(@redis/client@1.6.1) + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} retry@0.12.0: @@ -5752,6 +6080,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.1.0: {} rxjs@7.8.2: @@ -5784,6 +6122,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 @@ -5793,6 +6147,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: optional: true @@ -6055,6 +6418,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@5.9.3: {} uint8array-extras@1.5.0: {} diff --git a/server/corsair.ts b/server/corsair.ts index d91acf4..d909f7c 100644 --- a/server/corsair.ts +++ b/server/corsair.ts @@ -1,8 +1,8 @@ -import { createCorsair, slack } from 'corsair'; +import { createCorsair, linear, slack } from 'corsair'; import { pool } from './db'; export const corsair = createCorsair({ - plugins: [slack()], + plugins: [slack(), linear()], database: pool, kek: process.env.CORSAIR_KEK!, multiTenancy: false, diff --git a/server/mcp-server.ts b/server/mcp-server.ts new file mode 100644 index 0000000..226f287 --- /dev/null +++ b/server/mcp-server.ts @@ -0,0 +1,151 @@ +import { config } from 'dotenv'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; +config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../.env') }); +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import type { CorsairInspectMethods } from 'corsair/core'; +import { corsair } from './corsair'; + +const inspect = corsair as unknown as CorsairInspectMethods; + +const server = new McpServer({ + name: 'corsair', + version: '1.0.0', +}); + +// ───────────────────────────────────────────────────────────────────────────── +// list_operations +// Wraps corsair.list_operations(). Returns available endpoint paths, optionally +// filtered by plugin and/or type ('api' | 'webhooks' | 'db'). +// ───────────────────────────────────────────────────────────────────────────── + +server.tool( + 'list_operations', + "List available Corsair operations. Without options returns all API endpoints across every plugin. Filter by plugin (e.g. 'slack') and/or type ('api' | 'webhooks' | 'db').", + { + plugin: z.string().optional().describe("Plugin ID to filter by, e.g. 'slack' or 'github'"), + type: z + .enum(['api', 'webhooks', 'db']) + .optional() + .describe("Operation type: 'api' (default), 'webhooks', or 'db'"), + }, + async ({ plugin, type }) => { + const result = inspect.list_operations({ plugin, type }); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + }, +); + +// ───────────────────────────────────────────────────────────────────────────── +// get_schema +// Wraps corsair.get_schema(). Returns input/output schema + metadata for any +// endpoint, webhook, or db entity path returned by list_operations. +// ───────────────────────────────────────────────────────────────────────────── + +server.tool( + 'get_schema', + "Get the schema and metadata for a Corsair operation path. Accepts API paths ('slack.api.channels.list'), webhook paths ('slack.webhooks.messages.message'), or DB paths ('slack.db.messages.search').", + { + path: z + .string() + .describe("Full dot-path from list_operations, e.g. 'slack.api.channels.list'"), + }, + async ({ path }) => { + const result = inspect.get_schema(path); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + }, +); + +// ───────────────────────────────────────────────────────────────────────────── +// corsair_run +// Executes any API endpoint by its dot-path. Path format mirrors list_operations: +// plugin.api.group.method → corsair[plugin][api][group][method](args) +// e.g. 'slack.api.channels.list' → corsair.slack.api.channels.list(args) +// ───────────────────────────────────────────────────────────────────────────── + +server.tool( + 'corsair_run', + "Execute any Corsair API endpoint by its dot-path. Use list_operations to discover paths and get_schema to understand required args. Example path: 'slack.api.channels.list'.", + { + path: z + .string() + .describe("Full API dot-path, e.g. 'slack.api.messages.post'"), + args: z + .record(z.unknown()) + .default({}) + .describe('Arguments object for the operation'), + }, + async ({ path, args }) => { + const parts = path.split('.'); + + if (parts.length < 3) { + return { + isError: true, + content: [ + { + type: 'text', + text: `Invalid path "${path}". Expected format: "plugin.api.group.method". Use list_operations to see valid paths.`, + }, + ], + }; + } + + // Traverse: corsair → plugin → api → group → method + let fn: unknown = corsair; + for (const part of parts) { + if (typeof fn !== 'object' || fn === null) { + fn = undefined; + break; + } + fn = (fn as Record)[part]; + } + + if (typeof fn !== 'function') { + return { + isError: true, + content: [ + { + type: 'text', + text: `Path "${path}" is not a callable operation. Use list_operations to see valid paths.`, + }, + ], + }; + } + + try { + const result = await (fn as (args: unknown) => Promise)(args); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const extra = err instanceof Error && err.cause ? `\nCause: ${String(err.cause)}` : ''; + const full = JSON.stringify(err, Object.getOwnPropertyNames(err)); + console.error(`[corsair-mcp] corsair_run error for "${path}":`, err); + return { + isError: true, + content: [{ type: 'text', text: `Error running "${path}": ${message}${extra}\n${full}` }], + }; + } + }, +); + +// ───────────────────────────────────────────────────────────────────────────── +// Start +// ───────────────────────────────────────────────────────────────────────────── + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('[corsair-mcp] Server running on stdio'); +} + +main().catch((err) => { + console.error('[corsair-mcp] Fatal:', err); + process.exit(1); +}); From a7a35eac011f459ab23a6d809e66cfc1e660d976 Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Fri, 6 Mar 2026 15:55:05 +0530 Subject: [PATCH 02/12] feat: add MCP server integration and new test scripts for OpenAI and Claude SDKs --- .gitignore | 4 +- package.json | 7 +- pnpm-lock.yaml | 148 ++++++++++++++++++++++++++++++++++--- scripts/test-claude-sdk.ts | 18 +++++ scripts/test-mcp.ts | 16 ++++ scripts/test-openai.ts | 20 +++++ server/index.ts | 4 + server/mcp-http.ts | 82 ++++++++++++++++++++ server/mcp-server.ts | 139 +--------------------------------- server/mcp-tools.ts | 126 +++++++++++++++++++++++++++++++ server/sdk.ts | 69 +++++++++++++++++ 11 files changed, 484 insertions(+), 149 deletions(-) create mode 100644 scripts/test-claude-sdk.ts create mode 100644 scripts/test-mcp.ts create mode 100644 scripts/test-openai.ts create mode 100644 server/mcp-http.ts create mode 100644 server/mcp-tools.ts create mode 100644 server/sdk.ts diff --git a/.gitignore b/.gitignore index 97513a0..796e278 100644 --- a/.gitignore +++ b/.gitignore @@ -206,4 +206,6 @@ state.txt **/orginal-sdk/ **/fixtures -/store \ No newline at end of file +/store + +.claude/settings.local.json \ No newline at end of file diff --git a/package.json b/package.json index 8a81908..0af1b48 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,16 @@ "lint:fix": "biome check . --fix --unsafe", "test": "vitest run", "test:watch": "vitest", - "available-plugins": "tsx scripts/available-plugins.ts" + "available-plugins": "tsx scripts/available-plugins.ts", + "test:mcp": "tsx scripts/test-mcp.ts", + "test:openai": "tsx scripts/test-openai.ts", + "test:claude-sdk": "tsx scripts/test-claude-sdk.ts" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.52", + "@anthropic-ai/sdk": "^0.78.0", "@modelcontextprotocol/sdk": "^1.27.1", + "@openai/agents": "^0.5.4", "@trpc/server": "^11.10.0", "@whiskeysockets/baileys": "^7.0.0-rc.9", "chokidar": "^4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48cd842..2d68964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,15 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.52 version: 0.2.52(zod@3.25.76) + '@anthropic-ai/sdk': + specifier: ^0.78.0 + version: 0.78.0(zod@3.25.76) '@modelcontextprotocol/sdk': specifier: ^1.27.1 version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@openai/agents': + specifier: ^0.5.4 + version: 0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76) '@trpc/server': specifier: ^11.10.0 version: 11.10.0(typescript@5.9.3) @@ -40,7 +46,7 @@ importers: version: 1.40.0(encoding@0.1.13) mem0ai: specifier: ^2.2.3 - version: 2.2.3(@anthropic-ai/sdk@0.55.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)))(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0) + version: 2.2.3(@anthropic-ai/sdk@0.78.0(zod@3.25.76))(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)))(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@6.27.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0) node-cron: specifier: ^4.2.1 version: 4.2.1 @@ -87,9 +93,14 @@ packages: peerDependencies: zod: ^4.0.0 - '@anthropic-ai/sdk@0.55.1': - resolution: {integrity: sha512-gjOMS4chmm8BxClKmCjNHmvf1FrO1Cn++CSX6K3YCZjz5JG4I9ZttQ/xEH4FBsz6HQyZvnUpiKlOAkmxaGmEaQ==} + '@anthropic-ai/sdk@0.78.0': + resolution: {integrity: sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==} hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} @@ -158,6 +169,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@2.3.5': resolution: {integrity: sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg==} engines: {node: '>=14.21.3'} @@ -909,6 +924,29 @@ packages: engines: {node: '>=10'} deprecated: This functionality has been moved to @npmcli/fs + '@openai/agents-core@0.5.4': + resolution: {integrity: sha512-qAT9zGIIM7GT5/WGkLpp8Fuar7NL5qu30b5+o2jP3mE6aMfx9OZjdj0za/iYLeV5kzQ5pOcbvRXenfzHrhvd/A==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.5.4': + resolution: {integrity: sha512-1uDEu9iwM7oB3oWNxvT/yzkcr7WtjHe1ekbQOAsasEv9S0MKTT8uP2kknRVgxzgw+awTZBrhO2vfGhD1iKinuQ==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents-realtime@0.5.4': + resolution: {integrity: sha512-qlrhMWD3Xpzfrxplt/jvc1nlGtjNnRmyzgRAj6J5HX/bcnP0W4UdYHEJOreiIC8inj27kcVjQslyu0DAjVuXsA==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents@0.5.4': + resolution: {integrity: sha512-INstpf2vZ0rV6Zq9jcSzqq/oL2/D84YGGKCXnU2otAcQ0ji/VZm+zplDow/+oENnvKiXKdVtOrGMsXqNFL7W+Q==} + peerDependencies: + zod: ^4.0.0 + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -2245,6 +2283,10 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -2591,6 +2633,18 @@ packages: zod: optional: true + openai@6.27.0: + resolution: {integrity: sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -3083,6 +3137,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3322,7 +3379,11 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - '@anthropic-ai/sdk@0.55.1': {} + '@anthropic-ai/sdk@0.78.0(zod@3.25.76)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 3.25.76 '@azure/abort-controller@2.1.2': dependencies: @@ -3440,6 +3501,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/runtime@7.28.6': {} + '@biomejs/biome@2.3.5': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.3.5 @@ -3900,14 +3963,14 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76))': + '@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@6.27.0(ws@8.19.0)(zod@3.25.76))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.3.87(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)) + langsmith: 0.3.87(@opentelemetry/api@1.9.0)(openai@6.27.0(ws@8.19.0)(zod@3.25.76)) mustache: 4.2.0 p-queue: 6.6.2 p-retry: 4.6.2 @@ -3965,6 +4028,57 @@ snapshots: rimraf: 3.0.2 optional: true + '@openai/agents-core@0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76)': + dependencies: + debug: 4.4.3 + openai: 6.27.0(ws@8.19.0)(zod@3.25.76) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@openai/agents-core': 0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76) + debug: 4.4.3 + openai: 6.27.0(ws@8.19.0)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-realtime@0.5.4(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + dependencies: + '@openai/agents-core': 0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76) + '@types/ws': 8.18.1 + debug: 4.4.3 + ws: 8.19.0 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + + '@openai/agents@0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@openai/agents-core': 0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76) + '@openai/agents-openai': 0.5.4(@cfworker/json-schema@4.1.1)(ws@8.19.0)(zod@3.25.76) + '@openai/agents-realtime': 0.5.4(@cfworker/json-schema@4.1.1)(zod@3.25.76) + debug: 4.4.3 + openai: 6.27.0(ws@8.19.0)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - ws + '@opentelemetry/api@1.9.0': optional: true @@ -5407,6 +5521,11 @@ snapshots: dependencies: bignumber.js: 9.3.1 + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.6 + ts-algebra: 2.0.0 + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} @@ -5441,7 +5560,7 @@ snapshots: kysely@0.28.11: {} - langsmith@0.3.87(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)): + langsmith@0.3.87(@opentelemetry/api@1.9.0)(openai@6.27.0(ws@8.19.0)(zod@3.25.76)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 @@ -5451,7 +5570,7 @@ snapshots: uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - openai: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) + openai: 6.27.0(ws@8.19.0)(zod@3.25.76) lodash.includes@4.3.0: {} @@ -5521,14 +5640,14 @@ snapshots: media-typer@1.1.0: {} - mem0ai@2.2.3(@anthropic-ai/sdk@0.55.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)))(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0): + mem0ai@2.2.3(@anthropic-ai/sdk@0.78.0(zod@3.25.76))(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260302.0)(@google/genai@1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)))(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@6.27.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.14.0)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.97.0)(@types/jest@29.5.14)(@types/pg@8.16.0)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.3.0(encoding@0.1.13))(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.18.0)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.19.0): dependencies: - '@anthropic-ai/sdk': 0.55.1 + '@anthropic-ai/sdk': 0.78.0(zod@3.25.76) '@azure/identity': 4.13.0 '@azure/search-documents': 12.2.0 '@cloudflare/workers-types': 4.20260302.0 '@google/genai': 1.42.0(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)) - '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76)) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@6.27.0(ws@8.19.0)(zod@3.25.76)) '@mistralai/mistralai': 1.14.0 '@qdrant/js-client-rest': 1.13.0(typescript@5.9.3) '@supabase/supabase-js': 2.97.0 @@ -5773,6 +5892,11 @@ snapshots: transitivePeerDependencies: - encoding + openai@6.27.0(ws@8.19.0)(zod@3.25.76): + optionalDependencies: + ws: 8.19.0 + zod: 3.25.76 + p-finally@1.0.0: {} p-map@4.0.0: @@ -6400,6 +6524,8 @@ snapshots: tr46@0.0.3: {} + ts-algebra@2.0.0: {} + tslib@2.8.1: {} tsx@4.21.0: diff --git a/scripts/test-claude-sdk.ts b/scripts/test-claude-sdk.ts new file mode 100644 index 0000000..95fc8e7 --- /dev/null +++ b/scripts/test-claude-sdk.ts @@ -0,0 +1,18 @@ +import 'dotenv/config'; +import { query } from '@anthropic-ai/claude-agent-sdk'; +import { getCorsairMcp } from '../server/sdk'; + +const mcp = getCorsairMcp(); + +for await (const event of query({ + prompt: 'list all slack channels, send test message to #sdk-test channel', + options: { + model: 'claude-sonnet-4-6', + permissionMode: 'bypassPermissions', + mcpServers: { corsair: mcp }, + }, +})) { + if (event.type === 'result' && event.subtype === 'success') { + process.stdout.write(event.result); + } +} diff --git a/scripts/test-mcp.ts b/scripts/test-mcp.ts new file mode 100644 index 0000000..d159b3a --- /dev/null +++ b/scripts/test-mcp.ts @@ -0,0 +1,16 @@ +import 'dotenv/config'; +import Anthropic from '@anthropic-ai/sdk'; +import { getCorsairMcp } from '../server/sdk'; + +const anthropic = new Anthropic(); +const mcp = getCorsairMcp(); + +const response = await anthropic.beta.messages.create({ + model: 'claude-sonnet-4-6', + max_tokens: 4096, + mcp_servers: [{ type: 'url', name: 'corsair', url: mcp.url }], + messages: [{ role: 'user', content: 'List all available operations' }], + betas: ['mcp-client-2025-04-04'], +}); + +console.log(response.content); diff --git a/scripts/test-openai.ts b/scripts/test-openai.ts new file mode 100644 index 0000000..98e3a6c --- /dev/null +++ b/scripts/test-openai.ts @@ -0,0 +1,20 @@ +import 'dotenv/config'; +import { Agent, run, hostedMcpTool } from '@openai/agents'; +import { getCorsairMcp } from '../server/sdk'; + +const mcp = getCorsairMcp(); + +const agent = new Agent({ + name: 'Corsair Agent', + model: 'gpt-4o', + instructions: 'You are a helpful assistant with access to Corsair tools.', + tools: [ + hostedMcpTool({ + serverLabel: 'corsair', + serverUrl: mcp.url, + }), + ], +}); + +const result = await run(agent, 'list all slack channels'); +console.log(result.finalOutput); diff --git a/server/index.ts b/server/index.ts index bdf33cf..5b35a7d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -6,6 +6,7 @@ import express from 'express'; import type { SimpleMessage } from './agent'; import { runAgent, WORKFLOW_FAILURE_PROMPT } from './agent'; import { corsair } from './corsair'; +import { createMcpRouter } from './mcp-http'; import { db, permissions, @@ -320,6 +321,9 @@ async function main() { } }); + // ── MCP HTTP server (OpenAI / Anthropic agent integration) ─────────────── + app.use('/mcp', createMcpRouter()); + // ── tRPC router ─────────────────────────────────────────────────────────── app.use( '/trpc', diff --git a/server/mcp-http.ts b/server/mcp-http.ts new file mode 100644 index 0000000..4f6b87a --- /dev/null +++ b/server/mcp-http.ts @@ -0,0 +1,82 @@ +import { randomUUID } from 'crypto'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { Request, Response, Router } from 'express'; +import { Router as createRouter } from 'express'; +import { createMcpServer } from './mcp-tools'; + +interface Session { + server: McpServer; + transport: StreamableHTTPServerTransport; +} + +// In-memory session store — keyed by mcp-session-id header +const sessions = new Map(); + +function cleanup(sessionId: string) { + const session = sessions.get(sessionId); + if (session) { + session.transport.close(); + session.server.close(); + sessions.delete(sessionId); + } +} + +export function createMcpRouter(): Router { + const router = createRouter(); + + // POST — initialize new session or dispatch to existing one + router.post('/', async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (sessionId) { + const session = sessions.get(sessionId); + if (!session) { + res.status(404).json({ error: 'Session not found' }); + return; + } + await session.transport.handleRequest(req, res, req.body); + return; + } + + // New session + const server = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (id) => { + sessions.set(id, { server, transport }); + }, + }); + + res.on('close', () => { + const id = transport.sessionId; + if (id) { + // Delay cleanup so the session stays available for follow-up requests + setTimeout(() => cleanup(id), 60_000); + } + }); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + }); + + // GET — SSE push channel for an existing session + router.get('/', async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !sessions.has(sessionId)) { + res.status(400).json({ error: 'Missing or invalid mcp-session-id' }); + return; + } + const session = sessions.get(sessionId)!; + await session.transport.handleRequest(req, res); + }); + + // DELETE — explicit session teardown + router.delete('/', async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (sessionId) cleanup(sessionId); + res.status(200).end(); + }); + + return router; +} diff --git a/server/mcp-server.ts b/server/mcp-server.ts index 226f287..eeb4bdf 100644 --- a/server/mcp-server.ts +++ b/server/mcp-server.ts @@ -1,145 +1,12 @@ import { config } from 'dotenv'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; -config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../.env') }); -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +// config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../.env') }); import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { z } from 'zod'; -import type { CorsairInspectMethods } from 'corsair/core'; -import { corsair } from './corsair'; - -const inspect = corsair as unknown as CorsairInspectMethods; - -const server = new McpServer({ - name: 'corsair', - version: '1.0.0', -}); - -// ───────────────────────────────────────────────────────────────────────────── -// list_operations -// Wraps corsair.list_operations(). Returns available endpoint paths, optionally -// filtered by plugin and/or type ('api' | 'webhooks' | 'db'). -// ───────────────────────────────────────────────────────────────────────────── - -server.tool( - 'list_operations', - "List available Corsair operations. Without options returns all API endpoints across every plugin. Filter by plugin (e.g. 'slack') and/or type ('api' | 'webhooks' | 'db').", - { - plugin: z.string().optional().describe("Plugin ID to filter by, e.g. 'slack' or 'github'"), - type: z - .enum(['api', 'webhooks', 'db']) - .optional() - .describe("Operation type: 'api' (default), 'webhooks', or 'db'"), - }, - async ({ plugin, type }) => { - const result = inspect.list_operations({ plugin, type }); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], - }; - }, -); - -// ───────────────────────────────────────────────────────────────────────────── -// get_schema -// Wraps corsair.get_schema(). Returns input/output schema + metadata for any -// endpoint, webhook, or db entity path returned by list_operations. -// ───────────────────────────────────────────────────────────────────────────── - -server.tool( - 'get_schema', - "Get the schema and metadata for a Corsair operation path. Accepts API paths ('slack.api.channels.list'), webhook paths ('slack.webhooks.messages.message'), or DB paths ('slack.db.messages.search').", - { - path: z - .string() - .describe("Full dot-path from list_operations, e.g. 'slack.api.channels.list'"), - }, - async ({ path }) => { - const result = inspect.get_schema(path); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], - }; - }, -); - -// ───────────────────────────────────────────────────────────────────────────── -// corsair_run -// Executes any API endpoint by its dot-path. Path format mirrors list_operations: -// plugin.api.group.method → corsair[plugin][api][group][method](args) -// e.g. 'slack.api.channels.list' → corsair.slack.api.channels.list(args) -// ───────────────────────────────────────────────────────────────────────────── - -server.tool( - 'corsair_run', - "Execute any Corsair API endpoint by its dot-path. Use list_operations to discover paths and get_schema to understand required args. Example path: 'slack.api.channels.list'.", - { - path: z - .string() - .describe("Full API dot-path, e.g. 'slack.api.messages.post'"), - args: z - .record(z.unknown()) - .default({}) - .describe('Arguments object for the operation'), - }, - async ({ path, args }) => { - const parts = path.split('.'); - - if (parts.length < 3) { - return { - isError: true, - content: [ - { - type: 'text', - text: `Invalid path "${path}". Expected format: "plugin.api.group.method". Use list_operations to see valid paths.`, - }, - ], - }; - } - - // Traverse: corsair → plugin → api → group → method - let fn: unknown = corsair; - for (const part of parts) { - if (typeof fn !== 'object' || fn === null) { - fn = undefined; - break; - } - fn = (fn as Record)[part]; - } - - if (typeof fn !== 'function') { - return { - isError: true, - content: [ - { - type: 'text', - text: `Path "${path}" is not a callable operation. Use list_operations to see valid paths.`, - }, - ], - }; - } - - try { - const result = await (fn as (args: unknown) => Promise)(args); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const extra = err instanceof Error && err.cause ? `\nCause: ${String(err.cause)}` : ''; - const full = JSON.stringify(err, Object.getOwnPropertyNames(err)); - console.error(`[corsair-mcp] corsair_run error for "${path}":`, err); - return { - isError: true, - content: [{ type: 'text', text: `Error running "${path}": ${message}${extra}\n${full}` }], - }; - } - }, -); - -// ───────────────────────────────────────────────────────────────────────────── -// Start -// ───────────────────────────────────────────────────────────────────────────── +import { createMcpServer } from './mcp-tools'; async function main() { + const server = createMcpServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error('[corsair-mcp] Server running on stdio'); diff --git a/server/mcp-tools.ts b/server/mcp-tools.ts new file mode 100644 index 0000000..4369bff --- /dev/null +++ b/server/mcp-tools.ts @@ -0,0 +1,126 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import { corsair } from './corsair'; + +const inspect = corsair; + +export function createMcpServer(): McpServer { + const server = new McpServer({ + name: 'corsair', + version: '1.0.0', + }); + + // ───────────────────────────────────────────────────────────────────────── + // list_operations + // ───────────────────────────────────────────────────────────────────────── + + server.tool( + 'list_operations', + "List available Corsair operations. Without options returns all API endpoints across every plugin. Filter by plugin (e.g. 'slack') and/or type ('api' | 'webhooks' | 'db').", + { + plugin: z.string().optional().describe("Plugin ID to filter by, e.g. 'slack' or 'github'"), + type: z + .enum(['api', 'webhooks', 'db']) + .optional() + .describe("Operation type: 'api' (default), 'webhooks', or 'db'"), + }, + async ({ plugin, type }) => { + const result = inspect.list_operations({ plugin, type }); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + }, + ); + + // ───────────────────────────────────────────────────────────────────────── + // get_schema + // ───────────────────────────────────────────────────────────────────────── + + server.tool( + 'get_schema', + "Get the schema and metadata for a Corsair operation path. Accepts API paths ('slack.api.channels.list'), webhook paths ('slack.webhooks.messages.message'), or DB paths ('slack.db.messages.search').", + { + path: z + .string() + .describe("Full dot-path from list_operations, e.g. 'slack.api.channels.list'"), + }, + async ({ path }) => { + const result = inspect.get_schema(path); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + }, + ); + + // ───────────────────────────────────────────────────────────────────────── + // corsair_run + // ───────────────────────────────────────────────────────────────────────── + + server.tool( + 'corsair_run', + "Execute any Corsair API endpoint by its dot-path. Use list_operations to discover paths and get_schema to understand required args. Example path: 'slack.api.channels.list'.", + { + path: z + .string() + .describe("Full API dot-path, e.g. 'slack.api.messages.post'"), + args: z + .record(z.unknown()) + .default({}) + .describe('Arguments object for the operation'), + }, + async ({ path, args }) => { + const parts = path.split('.'); + + if (parts.length < 3) { + return { + isError: true, + content: [ + { + type: 'text', + text: `Invalid path "${path}". Expected format: "plugin.api.group.method". Use list_operations to see valid paths.`, + }, + ], + }; + } + + let fn: unknown = corsair; + for (const part of parts) { + if (typeof fn !== 'object' || fn === null) { + fn = undefined; + break; + } + fn = (fn as Record)[part]; + } + + if (typeof fn !== 'function') { + return { + isError: true, + content: [ + { + type: 'text', + text: `Path "${path}" is not a callable operation. Use list_operations to see valid paths.`, + }, + ], + }; + } + + try { + const result = await (fn as (args: unknown) => Promise)(args); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const extra = err instanceof Error && err.cause ? `\nCause: ${String(err.cause)}` : ''; + const full = JSON.stringify(err, Object.getOwnPropertyNames(err)); + console.error(`[corsair-mcp] corsair_run error for "${path}":`, err); + return { + isError: true, + content: [{ type: 'text', text: `Error running "${path}": ${message}${extra}\n${full}` }], + }; + } + }, + ); + + return server; +} diff --git a/server/sdk.ts b/server/sdk.ts new file mode 100644 index 0000000..8ce89fc --- /dev/null +++ b/server/sdk.ts @@ -0,0 +1,69 @@ +/** + * Corsair SDK adapters for OpenAI Agents and Anthropic Claude. + * + * ── OpenAI Agents SDK (@openai/agents) ──────────────────────────────────── + * + * import { Agent, run, hostedMcpTool } from '@openai/agents'; + * import { getCorsairMcp } from './server/sdk'; + * + * const agent = new Agent({ + * name: 'My Agent', + * model: 'gpt-4o', + * instructions: 'You are a helpful assistant.', + * tools: [hostedMcpTool({ serverLabel: 'corsair', serverUrl: getCorsairMcp().serverUrl })], + * }); + * const result = await run(agent, 'List my Slack channels'); + * console.log(result.finalOutput); + * + * ── Anthropic Claude API — remote MCP (mcp_servers beta) ────────────────── + * + * import Anthropic from '@anthropic-ai/sdk'; + * import { getAnthropicMcpServer } from './server/sdk'; + * + * const anthropic = new Anthropic(); + * const response = await (anthropic.beta as any).messages.create({ + * model: 'claude-sonnet-4-6', + * max_tokens: 4096, + * mcp_servers: [getAnthropicMcpServer()], + * messages: [{ role: 'user', content: 'List my Slack channels' }], + * }); + * + * ── Anthropic Claude Agent SDK — in-process MCP ─────────────────────────── + * + * See server/agent.ts — use buildMcpServer() which wires up the full + * agent tool suite (send_message, ask_human, manage_workflows, etc.) via + * createSdkMcpServer from @anthropic-ai/claude-agent-sdk. + * + * For just the Corsair API tools (list_operations / get_schema / corsair_run), + * point the agent at the HTTP /mcp endpoint using getAnthropicMcpServer(). + */ + +export interface CorsairMcp { + /** Required by Claude Agent SDK for HTTP MCP servers */ + type: 'http'; + /** MCP server URL — pass to hostedMcpTool({ serverUrl }) for OpenAI */ + url: string; + /** Auth headers (empty by default) */ + headers: Record; +} + +/** + * Returns the Corsair MCP config. Works with both SDKs: + * + * OpenAI Agents SDK: + * hostedMcpTool({ serverLabel: 'corsair', serverUrl: mcp.url, headers: mcp.headers }) + * + * Claude Agent SDK: + * mcpServers: { corsair: mcp } + * + * Anthropic API (beta): + * mcp_servers: [{ type: 'url', name: 'corsair', url: mcp.url }] + * + * Configure via .env: + * BASE_URL=https://your-domain.com (defaults to http://localhost:PORT) + */ +export function getCorsairMcp(): CorsairMcp { + const baseUrl = + process.env.BASE_URL ?? `http://localhost:${process.env.PORT ?? 3000}`; + return { type: 'http', url: `${baseUrl}/mcp`, headers: {} }; +} From c1e0fa8edb00d8d004d755aa0a878a323ea771a5 Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Fri, 6 Mar 2026 15:55:25 +0530 Subject: [PATCH 03/12] feat: add cron and webhook test scripts, enhance workflow management in MCP server --- package.json | 4 +- scripts/test-cron.ts | 18 ++++++ scripts/test-webhook.ts | 19 ++++++ server/executor.ts | 4 ++ server/mcp-tools.ts | 96 ++++++++++++++++++++++++++++ server/sdk.ts | 135 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 scripts/test-cron.ts create mode 100644 scripts/test-webhook.ts diff --git a/package.json b/package.json index 0af1b48..74b73c5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "available-plugins": "tsx scripts/available-plugins.ts", "test:mcp": "tsx scripts/test-mcp.ts", "test:openai": "tsx scripts/test-openai.ts", - "test:claude-sdk": "tsx scripts/test-claude-sdk.ts" + "test:claude-sdk": "tsx scripts/test-claude-sdk.ts", + "test:webhook": "tsx scripts/test-webhook.ts", + "test:cron": "tsx scripts/test-cron.ts" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.52", diff --git a/scripts/test-cron.ts b/scripts/test-cron.ts new file mode 100644 index 0000000..ecf41da --- /dev/null +++ b/scripts/test-cron.ts @@ -0,0 +1,18 @@ +import 'dotenv/config'; +import { corsair } from '../server/corsair'; // imported for type safety — injected at runtime +import { workflow } from '../server/sdk'; + +await workflow.cron({ + id: 'morningSlackReport', + description: 'Posts a morning message to #sdk-test every weekday at 9am', + schedule: '0 9 * * 1-5', + handler: async () => { + await corsair.slack.api.messages.post({ + channel: 'C0A3ZTB9X7X', + text: 'Good morning! Your daily report is ready.', + }); + }, +}); + +const workflows = await workflow.list('cron'); +console.table(workflows.map((w) => ({ name: w.name, schedule: (w.triggerConfig as any)?.cron, status: w.status }))); diff --git a/scripts/test-webhook.ts b/scripts/test-webhook.ts new file mode 100644 index 0000000..df188da --- /dev/null +++ b/scripts/test-webhook.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { corsair } from '../server/corsair'; // imported for type safety — injected at runtime +import { workflow } from '../server/sdk'; + +await workflow.webhook({ + id: 'forwardSlackMessageToSdkTest', + description: 'Forwards every Slack message to #sdk-test', + trigger: { plugin: 'slack', action: 'messages.message' }, + handler: async (event: any) => { + if (!event?.text || event?.channel === 'C0A3ZTB9X7X') return; + await corsair.slack.api.messages.post({ + channel: 'C0A3ZTB9X7X', + text: `[forwarded] <@${event.user}>: ${event.text}`, + }); + }, +}); + +const workflows = await workflow.list('webhook'); +console.table(workflows.map((w) => ({ name: w.name, trigger: JSON.stringify(w.triggerConfig), status: w.status }))); diff --git a/server/executor.ts b/server/executor.ts index 3b59801..738207c 100644 --- a/server/executor.ts +++ b/server/executor.ts @@ -327,6 +327,8 @@ export async function listWorkflows( })); } +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + export async function findWorkflowByNameOrId(nameOrId: string) { const byName = await db .select() @@ -335,6 +337,8 @@ export async function findWorkflowByNameOrId(nameOrId: string) { .limit(1); if (byName[0]) return byName[0]; + if (!UUID_RE.test(nameOrId)) return null; + const byId = await db .select() .from(workflows) diff --git a/server/mcp-tools.ts b/server/mcp-tools.ts index 4369bff..ec6226e 100644 --- a/server/mcp-tools.ts +++ b/server/mcp-tools.ts @@ -1,6 +1,16 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { corsair } from './corsair'; +import { + archiveWorkflow, + listWorkflows, + storeWorkflow, + updateWorkflowRecord, +} from './executor'; +import { + registerCronWorkflow, + unregisterCronWorkflow, +} from './workflow-scheduler'; const inspect = corsair; @@ -122,5 +132,91 @@ export function createMcpServer(): McpServer { }, ); + // ───────────────────────────────────────────────────────────────────────── + // manage_workflows + // Create / list / update / delete webhook and cron workflows. + // ───────────────────────────────────────────────────────────────────────── + + server.tool( + 'manage_workflows', + 'List, create, update, or delete Corsair workflows. Use action="create" with webhookTrigger to register a webhook-triggered workflow, or cronSchedule for a cron workflow.', + { + action: z.enum(['list', 'create', 'update', 'delete']).describe('Action to perform'), + triggerType: z + .enum(['cron', 'webhook', 'manual', 'all']) + .optional() + .describe('Filter by trigger type (list only)'), + workflowId: z.string().optional().describe('Workflow function name — required for create/update/delete'), + code: z.string().optional().describe('TypeScript workflow code — required for create'), + description: z.string().optional().describe('Human-readable description'), + cronSchedule: z.string().optional().describe('Cron expression, e.g. "0 9 * * 1-5"'), + webhookTrigger: z + .object({ plugin: z.string(), action: z.string() }) + .optional() + .describe('Webhook trigger, e.g. { plugin: "slack", action: "messages.message" }'), + status: z.enum(['active', 'paused', 'archived']).optional().describe('Workflow status (update only)'), + }, + async ({ action, triggerType, workflowId, code, description, cronSchedule, webhookTrigger, status }) => { + let result: unknown; + + if (action === 'list') { + result = { workflows: await listWorkflows(triggerType) }; + } else if (action === 'create') { + if (!workflowId || !code) { + result = { success: false, error: 'workflowId and code are required for create' }; + } else { + const stored = await storeWorkflow({ + type: 'workflow', + workflowId, + code, + description: description?.trim() || undefined, + cronSchedule: cronSchedule?.trim() || undefined, + webhookTrigger, + }); + if (stored && cronSchedule?.trim()) { + const ok = registerCronWorkflow(stored.id, workflowId, code, cronSchedule.trim()); + if (!ok) { + result = { success: false, error: `Invalid cron expression: "${cronSchedule}"` }; + return { content: [{ type: 'text', text: JSON.stringify(result) }] }; + } + } + result = { success: true, workflow: { id: stored!.id, name: stored!.name, triggerType: stored!.triggerType, status: stored!.status } }; + } + } else if (action === 'delete') { + if (!workflowId) { + result = { success: false, error: 'workflowId is required for delete' }; + } else { + const archived = await archiveWorkflow(workflowId); + if (!archived) { + result = { success: false, error: `Workflow "${workflowId}" not found` }; + } else { + unregisterCronWorkflow(archived.id); + result = { success: true, message: `Workflow "${archived.name}" archived` }; + } + } + } else { + // update + if (!workflowId) { + result = { success: false, error: 'workflowId is required for update' }; + } else { + const updated = await updateWorkflowRecord(workflowId, { code, description, cronSchedule, webhookTrigger, status }); + if (!updated) { + result = { success: false, error: `Workflow "${workflowId}" not found` }; + } else { + if (updated.status === 'archived' || updated.status === 'paused') { + unregisterCronWorkflow(updated.id); + } else if (updated.triggerType === 'cron') { + const cfg = updated.triggerConfig as { cron?: string }; + if (cfg.cron) registerCronWorkflow(updated.id, updated.name, updated.code, cfg.cron); + } + result = { success: true, workflow: { id: updated.id, name: updated.name, triggerType: updated.triggerType, status: updated.status } }; + } + } + } + + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + }, + ); + return server; } diff --git a/server/sdk.ts b/server/sdk.ts index 8ce89fc..6fb4382 100644 --- a/server/sdk.ts +++ b/server/sdk.ts @@ -67,3 +67,138 @@ export function getCorsairMcp(): CorsairMcp { process.env.BASE_URL ?? `http://localhost:${process.env.PORT ?? 3000}`; return { type: 'http', url: `${baseUrl}/mcp`, headers: {} }; } + +// ───────────────────────────────────────────────────────────────────────────── +// workflow — programmatic webhook & cron registration +// +// Usage: +// import { workflow } from './server/sdk'; +// +// await workflow.webhook({ +// id: 'myWebhook', +// trigger: { plugin: 'slack', action: 'messages.message' }, +// handler: async (event: any) => { +// await corsair.slack.api.messages.post({ channel: 'C123', text: event.text }); +// }, +// }); +// +// await workflow.cron({ +// id: 'myReport', +// schedule: '0 9 * * 1-5', +// handler: async () => { +// await corsair.slack.api.messages.post({ channel: 'C123', text: 'Morning!' }); +// }, +// }); +// +// Note: handlers run inside the server process via tsx. `corsair` is injected +// automatically — import it in your script only for type safety, not for runtime. +// ───────────────────────────────────────────────────────────────────────────── + +import { + archiveWorkflow, + findWorkflowByNameOrId, + listWorkflows, + storeWorkflow, + updateWorkflowRecord, +} from './executor'; +import { registerCronWorkflow } from './workflow-scheduler'; + +function serializeWebhookHandler(id: string, fn: (event: unknown) => Promise): string { + return [ + `export async function ${id}() {`, + ` // Handles both Slack's { event: {...} } wrapper and flat payloads`, + ` const __payload = (__event as any)?.event ?? __event;`, + ` const __handler = ${fn.toString()};`, + ` return __handler(__payload);`, + `}`, + ].join('\n'); +} + +function serializeCronHandler(id: string, fn: () => Promise): string { + return [ + `export async function ${id}() {`, + ` const __handler = ${fn.toString()};`, + ` return __handler();`, + `}`, + ].join('\n'); +} + +export const workflow = { + /** + * Register or update a webhook-triggered workflow. + * The handler is serialized and stored in the DB. `corsair` is available as a global. + */ + async webhook(opts: { + id: string; + description?: string; + trigger: { plugin: string; action: string }; + handler: (event: T) => Promise; + }) { + const code = serializeWebhookHandler(opts.id, opts.handler as (event: unknown) => Promise); + const existing = await findWorkflowByNameOrId(opts.id); + if (existing) { + const updated = await updateWorkflowRecord(opts.id, { + code, + description: opts.description, + webhookTrigger: opts.trigger, + }); + console.log(`[workflow] Updated webhook workflow "${opts.id}"`); + return updated; + } + const stored = await storeWorkflow({ + type: 'workflow', + workflowId: opts.id, + code, + description: opts.description, + webhookTrigger: opts.trigger, + }); + console.log(`[workflow] Registered webhook workflow "${opts.id}"`); + return stored; + }, + + /** + * Register or update a cron-triggered workflow. + * The handler is serialized and stored in the DB. `corsair` is available as a global. + * Note: the cron scheduler in the running server picks up new workflows on restart. + */ + async cron(opts: { + id: string; + description?: string; + schedule: string; + handler: () => Promise; + }) { + const code = serializeCronHandler(opts.id, opts.handler); + const existing = await findWorkflowByNameOrId(opts.id); + if (existing) { + const updated = await updateWorkflowRecord(opts.id, { + code, + description: opts.description, + cronSchedule: opts.schedule, + }); + console.log(`[workflow] Updated cron workflow "${opts.id}" (${opts.schedule})`); + return updated; + } + const stored = await storeWorkflow({ + type: 'workflow', + workflowId: opts.id, + code, + description: opts.description, + cronSchedule: opts.schedule, + }); + if (stored) { + registerCronWorkflow(stored.id, opts.id, code, opts.schedule); + } + console.log(`[workflow] Registered cron workflow "${opts.id}" (${opts.schedule})`); + return stored; + }, + + /** List workflows. Optionally filter by type. */ + list(triggerType?: 'cron' | 'webhook' | 'manual' | 'all') { + return listWorkflows(triggerType); + }, + + /** Archive (soft-delete) a workflow by name or ID. */ + delete(id: string) { + return archiveWorkflow(id); + }, +}; From 4485b1341504f8b50c266686f57a7958305c424a Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Fri, 6 Mar 2026 17:36:07 +0530 Subject: [PATCH 04/12] feat: enhance workflow execution and update environment configuration in MCP server --- server/db/index.ts | 5 ++++- server/executor.ts | 6 ++++-- server/mcp-server.ts | 2 +- server/mcp-tools.ts | 12 +++++++++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/server/db/index.ts b/server/db/index.ts index 7e6c66d..028054c 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -1,11 +1,14 @@ import { config } from 'dotenv'; import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; import * as schema from './schema'; // Load env as early as possible (import order matters with ESM). -config({ path: '.env' }); +// Use an absolute path so this works regardless of cwd (e.g. when spawned by Claude Desktop MCP). +config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../../.env') }); // ───────────────────────────────────────────────────────────────────────────── // Connection (shared with Corsair) diff --git a/server/executor.ts b/server/executor.ts index 738207c..3cc73a5 100644 --- a/server/executor.ts +++ b/server/executor.ts @@ -38,11 +38,13 @@ export async function executeWorkflow( const wrappedCode = ` import { corsair } from './server/corsair'; const __event: unknown = ${JSON.stringify(eventPayload ?? null)}; +const ctx = { sdk: corsair }; ${code} -// Execute the workflow -${workflowId}().catch(console.error); +// Execute the workflow — pass ctx and the event payload so workflow functions +// can use either ctx.sdk. or the top-level corsair import directly. +${workflowId}(ctx, __event).catch(console.error); `; try { diff --git a/server/mcp-server.ts b/server/mcp-server.ts index eeb4bdf..7dbedb0 100644 --- a/server/mcp-server.ts +++ b/server/mcp-server.ts @@ -1,7 +1,7 @@ import { config } from 'dotenv'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; -// config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../.env') }); +config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../.env') }); import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { createMcpServer } from './mcp-tools'; diff --git a/server/mcp-tools.ts b/server/mcp-tools.ts index ec6226e..83f5a19 100644 --- a/server/mcp-tools.ts +++ b/server/mcp-tools.ts @@ -139,7 +139,17 @@ export function createMcpServer(): McpServer { server.tool( 'manage_workflows', - 'List, create, update, or delete Corsair workflows. Use action="create" with webhookTrigger to register a webhook-triggered workflow, or cronSchedule for a cron workflow.', + `List, create, update, or delete Corsair workflows. Use action="create" with webhookTrigger to register a webhook-triggered workflow, or cronSchedule for a cron workflow. + +WORKFLOW CODE CONTRACT: +- The workflow function receives two arguments: ctx and payload. + - ctx.sdk is the full corsair SDK (same as the corsair import), e.g. ctx.sdk.slack.api.messages.post(...) + - payload is the raw webhook event payload (for webhook-triggered workflows) or undefined (for cron/manual). +- Function signature: async function (ctx: { sdk: typeof import('./corsair').corsair }, payload?: unknown) +- Example: + async function myWorkflow(ctx, payload) { + await ctx.sdk.slack.api.messages.post({ channel: 'C12345', text: 'hello' }); + }`, { action: z.enum(['list', 'create', 'update', 'delete']).describe('Action to perform'), triggerType: z From 4a97c8ad25d241e04433bfac25a8c4f26e1492aa Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Fri, 6 Mar 2026 19:56:33 +0530 Subject: [PATCH 05/12] feat: update OpenAI model to gpt-4.1 and add description to MCP server --- scripts/test-openai.ts | 2 +- server/mcp-tools.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/test-openai.ts b/scripts/test-openai.ts index 98e3a6c..dee48b4 100644 --- a/scripts/test-openai.ts +++ b/scripts/test-openai.ts @@ -6,7 +6,7 @@ const mcp = getCorsairMcp(); const agent = new Agent({ name: 'Corsair Agent', - model: 'gpt-4o', + model: 'gpt-4.1', instructions: 'You are a helpful assistant with access to Corsair tools.', tools: [ hostedMcpTool({ diff --git a/server/mcp-tools.ts b/server/mcp-tools.ts index 83f5a19..eaad9ce 100644 --- a/server/mcp-tools.ts +++ b/server/mcp-tools.ts @@ -18,6 +18,7 @@ export function createMcpServer(): McpServer { const server = new McpServer({ name: 'corsair', version: '1.0.0', + description: 'Use this to interact with the Corsair API. Corsair helps you integrate with dozens of tools and services. You can setup cron jobs and webhooks triggered jobs.' }); // ───────────────────────────────────────────────────────────────────────── From 7ff99568f157c27a3e1a80ebb142282774f1f159 Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Mon, 9 Mar 2026 13:08:07 +0530 Subject: [PATCH 06/12] feat: mcp package --- server/agent.ts | 272 +++++------------------------------------ server/index.ts | 28 ++++- server/mcp-adapters.ts | 96 +++++++++++++++ server/mcp-http.ts | 82 ------------- server/mcp-server.ts | 20 ++- server/mcp-tools.ts | 233 ----------------------------------- server/script.ts | 2 +- 7 files changed, 167 insertions(+), 566 deletions(-) create mode 100644 server/mcp-adapters.ts delete mode 100644 server/mcp-http.ts delete mode 100644 server/mcp-tools.ts diff --git a/server/agent.ts b/server/agent.ts index b7f475b..17b4f46 100644 --- a/server/agent.ts +++ b/server/agent.ts @@ -3,19 +3,16 @@ import { query, tool, } from '@anthropic-ai/claude-agent-sdk'; +import { createClaudeTools } from '@corsair/mcp'; import { desc, eq } from 'drizzle-orm'; import { z } from 'zod'; -import { db, permissions, whatsappMessages } from './db'; +import { db, whatsappMessages } from './db'; import { - archiveWorkflow, - listWorkflows, - storeWorkflow, - updateWorkflowRecord, -} from './executor'; -import { - registerCronWorkflow, - unregisterCronWorkflow, -} from './workflow-scheduler'; + cronAdapter, + permissionAdapter, + workflowAdapter, +} from './mcp-adapters'; +import { corsair } from './corsair'; // ───────────────────────────────────────────────────────────────────────────── // Types @@ -164,11 +161,23 @@ function buildSystemPromptWithHistory( // In-process MCP server // ───────────────────────────────────────────────────────────────────────────── -export function buildMcpServer(context?: { +export async function buildMcpServer(context?: { jid?: string; onMessage?: (text: string) => Promise; onAskHuman?: (question: string) => void; }) { + const basePermissionUrl = + process.env.BASE_PERMISSION_URL ?? + process.env.BASE_URL ?? + `http://localhost:${process.env.PORT ?? 3000}`; + const corsairTools = await createClaudeTools({ + corsair, + workflows: workflowAdapter, + cron: cronAdapter, + permissions: permissionAdapter, + basePermissionUrl, + context, + }); const sendMessageTool = tool( 'send_message', 'Send a message to the user without pausing. Use for acknowledgments ("On it!"), progress updates, and final answers. You can call it multiple times.', @@ -197,228 +206,6 @@ export function buildMcpServer(context?: { }, ); - const requestPermissionTool = tool( - 'request_permission', - 'Request permission from the user to execute a protected endpoint. Call this when a script returns a [PERMISSION_REQUIRED] message. Returns an approval URL. After calling this, call ask_human with the approval URL so the user can review and approve.', - { - endpoint: z - .string() - .describe( - 'Full endpoint path from the PERMISSION_REQUIRED message, e.g. "slack.messages.post"', - ), - args: z - .record(z.unknown()) - .describe('The arguments object from the PERMISSION_REQUIRED message'), - description: z - .string() - .describe( - 'Short human-readable summary of what this action will do, e.g. "Post a message to #general in Slack"', - ), - }, - async ({ endpoint, args, description }) => { - const [plugin, ...rest] = endpoint.split('.'); - const operation = rest.join('.'); - - const [perm] = await db - .insert(permissions) - .values({ - endpoint, - plugin: plugin!, - operation, - args, - description, - status: 'pending', - jid: context?.jid, - }) - .returning({ id: permissions.id }); - - const baseUrl = process.env.BASE_PERMISSION_URL; - const approvalUrl = `${baseUrl}/permissions/${perm!.id}`; - - const result = { - permissionId: perm!.id, - approvalUrl, - message: `Permission request created. Ask the user to approve at: ${approvalUrl}`, - }; - return { - content: [{ type: 'text' as const, text: JSON.stringify(result) }], - }; - }, - ); - - const manageWorkflowsTool = tool( - 'manage_workflows', - 'List (optional triggerType filter), create (store a new workflow), update (workflowId + fields), or archive (workflowId) workflows.', - { - action: z - .enum(['list', 'create', 'update', 'delete']) - .describe('The action to perform'), - triggerType: z - .enum(['cron', 'webhook', 'manual', 'all']) - .optional() - .describe('Filter workflows by trigger type (for list)'), - workflowId: z - .string() - .optional() - .describe('Workflow ID (function name) for create/update/delete'), - code: z.string().optional().describe('Workflow code for create/update'), - description: z.string().optional().describe('Human-readable description'), - cronSchedule: z - .string() - .optional() - .describe('Cron schedule for create/update'), - webhookTrigger: z - .object({ plugin: z.string(), action: z.string() }) - .optional() - .describe('Webhook trigger for create/update'), - status: z - .enum(['active', 'paused', 'archived']) - .optional() - .describe('Workflow status for update'), - }, - async ({ - action, - triggerType, - workflowId, - code, - description, - cronSchedule, - webhookTrigger, - status, - }) => { - console.log( - `[manage_workflows] action=${action} workflowId=${workflowId ?? 'N/A'} cronSchedule=${cronSchedule ?? 'N/A'} codeLen=${code?.length ?? 0}`, - ); - let result: unknown; - - if (action === 'list') { - result = { workflows: await listWorkflows(triggerType) }; - } else if (action === 'create') { - if (!workflowId || !code) { - result = { - success: false, - error: 'workflowId and code are required for create', - }; - } else { - const stored = await storeWorkflow({ - type: 'workflow', - workflowId, - code, - description: description?.trim() || undefined, - cronSchedule: cronSchedule?.trim() || undefined, - webhookTrigger, - notifyJid: context?.jid, - }); - console.log( - `[manage_workflows] storeWorkflow → id=${stored?.id} name=${stored?.name} triggerType=${stored?.triggerType}`, - ); - // Register with the cron scheduler immediately if it's a cron workflow - if (stored && cronSchedule?.trim()) { - const ok = registerCronWorkflow( - stored.id, - workflowId, - code, - cronSchedule.trim(), - context?.jid, - ); - console.log(`[manage_workflows] registerCronWorkflow → ok=${ok}`); - if (!ok) { - result = { - success: false, - error: `Invalid cron expression: "${cronSchedule}"`, - }; - return { - content: [ - { type: 'text' as const, text: JSON.stringify(result) }, - ], - }; - } - } - result = { - success: true, - workflow: { - id: stored!.id, - name: stored!.name, - triggerType: stored!.triggerType, - status: stored!.status, - }, - }; - } - } else if (action === 'delete') { - if (!workflowId) { - result = { - success: false, - error: 'workflowId is required for delete', - }; - } else { - const archived = await archiveWorkflow(workflowId); - if (!archived) { - result = { - success: false, - error: `Workflow "${workflowId}" not found`, - }; - } else { - unregisterCronWorkflow(archived.id); - result = { - success: true, - message: `Workflow "${archived.name}" archived`, - }; - } - } - } else { - // action === 'update' - if (!workflowId) { - result = { - success: false, - error: 'workflowId is required for update', - }; - } else { - const updated = await updateWorkflowRecord(workflowId, { - code, - description, - cronSchedule, - webhookTrigger, - status, - }); - if (!updated) { - result = { - success: false, - error: `Workflow "${workflowId}" not found`, - }; - } else { - // Sync the in-memory scheduler with the new DB state - if (updated.status === 'archived' || updated.status === 'paused') { - unregisterCronWorkflow(updated.id); - } else if (updated.triggerType === 'cron') { - const cfg = updated.triggerConfig as { cron?: string }; - if (cfg.cron) { - registerCronWorkflow( - updated.id, - updated.name, - updated.code, - cfg.cron, - ); - } - } - result = { - success: true, - workflow: { - id: updated.id, - name: updated.name, - triggerType: updated.triggerType, - status: updated.status, - }, - }; - } - } - } - - return { - content: [{ type: 'text' as const, text: JSON.stringify(result) }], - }; - }, - ); - const getConversationHistoryTool = tool( 'get_conversation_history', 'Fetch past messages from the current WhatsApp chat. Use when you need more context about what the user said earlier. Start with a small limit and call again with a larger one if needed.', @@ -475,12 +262,11 @@ export function buildMcpServer(context?: { name: 'corsair', version: '1.0.0', tools: [ + ...corsairTools, sendMessageTool, askHumanTool, - requestPermissionTool, - manageWorkflowsTool, getConversationHistoryTool, - ], + ] as Parameters[0]['tools'], }); } @@ -510,12 +296,11 @@ export async function runAgent( ); const abortController = new AbortController(); let askHumanQuestion: string | null = null; - const mcpServer = buildMcpServer({ + const mcpServer = await buildMcpServer({ jid: opts.jid, onMessage, onAskHuman: (question) => { askHumanQuestion = question; - // Defer abort so the tool handler returns cleanly before the SDK is stopped process.nextTick(() => abortController.abort()); }, }); @@ -602,13 +387,12 @@ export async function* createAgentStream( const systemPrompt = buildSystemPromptWithHistory(opts.history); const abortController = new AbortController(); let askHumanQuestion: string | null = null; - const mcpServer = buildMcpServer({ + const mcpServer = await buildMcpServer({ onAskHuman: (question) => { askHumanQuestion = question; process.nextTick(() => abortController.abort()); }, }); - // Map from toolCallId → toolName so we can emit tool-result with name const pendingToolCalls = new Map(); try { @@ -658,14 +442,14 @@ export async function* createAgentStream( typeof block === 'object' && block !== null && 'type' in block && - (block satisfies { type: string }).type === 'tool_result' + block.type === 'tool_result' && + 'tool_use_id' in block ) { - const toolResult = block satisfies { tool_use_id: string }; const toolName = - pendingToolCalls.get(toolResult.tool_use_id) ?? 'unknown'; + pendingToolCalls.get(block.tool_use_id) ?? 'unknown'; yield { type: 'tool-result', - toolCallId: toolResult.tool_use_id, + toolCallId: block.tool_use_id, toolName, }; } diff --git a/server/index.ts b/server/index.ts index 5b35a7d..5f7d907 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,8 +5,13 @@ import { asc, eq } from 'drizzle-orm'; import express from 'express'; import type { SimpleMessage } from './agent'; import { runAgent, WORKFLOW_FAILURE_PROMPT } from './agent'; +import { createBaseMcpServer, createMcpRouter } from '@corsair/mcp'; import { corsair } from './corsair'; -import { createMcpRouter } from './mcp-http'; +import { + cronAdapter, + permissionAdapter, + workflowAdapter, +} from './mcp-adapters'; import { db, permissions, @@ -220,7 +225,7 @@ async function main() { res: import('express').Response, ) { try { - const pluginKeys = corsair[plugin].keys; + const pluginKeys = (corsair as unknown as Record Promise<{ client_id?: string; redirect_url?: string; client_secret?: string }>; set_access_token: (t: string) => Promise; set_refresh_token: (t: string) => Promise } }>)[plugin].keys; const creds = await pluginKeys.get_integration_credentials(); if (!creds.client_id || !creds.redirect_url) { res.status(400).send(`${plugin} not configured. Run the setup script first.`); @@ -263,7 +268,7 @@ async function main() { const { label } = GOOGLE_PLUGIN_CONFIG[plugin]; try { - const pluginKeys = corsair[plugin].keys; + const pluginKeys = (corsair as unknown as Record Promise<{ client_id?: string; redirect_url?: string; client_secret?: string }>; set_access_token: (t: string) => Promise; set_refresh_token: (t: string) => Promise } }>)[plugin].keys; const creds = await pluginKeys.get_integration_credentials(); if (!creds.client_id || !creds.client_secret || !creds.redirect_url) { res.status(400).send('Missing integration credentials.'); @@ -322,7 +327,22 @@ async function main() { }); // ── MCP HTTP server (OpenAI / Anthropic agent integration) ─────────────── - app.use('/mcp', createMcpRouter()); + const basePermissionUrl = + process.env.BASE_PERMISSION_URL ?? + process.env.BASE_URL ?? + `http://localhost:${process.env.PORT ?? 3000}`; + app.use( + '/mcp', + createMcpRouter(() => + createBaseMcpServer({ + corsair, + workflows: workflowAdapter, + cron: cronAdapter, + permissions: permissionAdapter, + basePermissionUrl, + }), + ), + ); // ── tRPC router ─────────────────────────────────────────────────────────── app.use( diff --git a/server/mcp-adapters.ts b/server/mcp-adapters.ts new file mode 100644 index 0000000..3f4dca1 --- /dev/null +++ b/server/mcp-adapters.ts @@ -0,0 +1,96 @@ +import type { + CronAdapter, + PermissionAdapter, + WorkflowAdapter, + WorkflowListItem, + WorkflowStored, + WorkflowStoreInput, + WorkflowUpdateFields, +} from '@corsair/mcp'; +import { db, permissions } from './db'; +import { + archiveWorkflow, + listWorkflows, + storeWorkflow, + updateWorkflowRecord, +} from './executor'; +import { + registerCronWorkflow, + unregisterCronWorkflow, +} from './workflow-scheduler'; + +function toStored( + row: { + id: string; + name: string; + triggerType: 'manual' | 'cron' | 'webhook'; + status: string; + code?: string; + triggerConfig?: unknown; + }, +): WorkflowStored { + const triggerConfig = + row.triggerConfig != null && + typeof row.triggerConfig === 'object' && + !Array.isArray(row.triggerConfig) + ? (row.triggerConfig as Record) + : undefined; + return { + id: row.id, + name: row.name, + triggerType: row.triggerType, + status: row.status, + code: row.code, + triggerConfig, + }; +} + +export const workflowAdapter: WorkflowAdapter = { + async listWorkflows(triggerType) { + const rows = await listWorkflows(triggerType); + return rows as WorkflowListItem[]; + }, + async storeWorkflow(input: WorkflowStoreInput) { + const row = await storeWorkflow(input); + return row ? toStored(row) : null; + }, + async updateWorkflowRecord(nameOrId, updates: WorkflowUpdateFields) { + const row = await updateWorkflowRecord(nameOrId, updates); + return row ? toStored(row) : null; + }, + async archiveWorkflow(nameOrId) { + const row = await archiveWorkflow(nameOrId); + return row ? toStored(row) : null; + }, +}; + +export const cronAdapter: CronAdapter = { + registerCronWorkflow(dbId, name, code, schedule, notifyJid) { + return registerCronWorkflow(dbId, name, code, schedule, notifyJid); + }, + unregisterCronWorkflow(dbId) { + unregisterCronWorkflow(dbId); + }, +}; + +export const permissionAdapter: PermissionAdapter = { + async createPermissionRequest({ endpoint, args, description, jid }) { + const [plugin, ...rest] = endpoint.split('.'); + const operation = rest.join('.'); + const [perm] = await db + .insert(permissions) + .values({ + endpoint, + plugin: plugin!, + operation, + args, + description, + status: 'pending', + jid: jid ?? null, + }) + .returning({ id: permissions.id }); + const baseUrl = process.env.BASE_PERMISSION_URL ?? process.env.BASE_URL ?? `http://localhost:${process.env.PORT ?? 3000}`; + const approvalUrl = `${baseUrl}/permissions/${perm!.id}`; + return { permissionId: perm!.id, approvalUrl }; + }, +}; diff --git a/server/mcp-http.ts b/server/mcp-http.ts deleted file mode 100644 index 4f6b87a..0000000 --- a/server/mcp-http.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { randomUUID } from 'crypto'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { Request, Response, Router } from 'express'; -import { Router as createRouter } from 'express'; -import { createMcpServer } from './mcp-tools'; - -interface Session { - server: McpServer; - transport: StreamableHTTPServerTransport; -} - -// In-memory session store — keyed by mcp-session-id header -const sessions = new Map(); - -function cleanup(sessionId: string) { - const session = sessions.get(sessionId); - if (session) { - session.transport.close(); - session.server.close(); - sessions.delete(sessionId); - } -} - -export function createMcpRouter(): Router { - const router = createRouter(); - - // POST — initialize new session or dispatch to existing one - router.post('/', async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - - if (sessionId) { - const session = sessions.get(sessionId); - if (!session) { - res.status(404).json({ error: 'Session not found' }); - return; - } - await session.transport.handleRequest(req, res, req.body); - return; - } - - // New session - const server = createMcpServer(); - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (id) => { - sessions.set(id, { server, transport }); - }, - }); - - res.on('close', () => { - const id = transport.sessionId; - if (id) { - // Delay cleanup so the session stays available for follow-up requests - setTimeout(() => cleanup(id), 60_000); - } - }); - - await server.connect(transport); - await transport.handleRequest(req, res, req.body); - }); - - // GET — SSE push channel for an existing session - router.get('/', async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !sessions.has(sessionId)) { - res.status(400).json({ error: 'Missing or invalid mcp-session-id' }); - return; - } - const session = sessions.get(sessionId)!; - await session.transport.handleRequest(req, res); - }); - - // DELETE — explicit session teardown - router.delete('/', async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (sessionId) cleanup(sessionId); - res.status(200).end(); - }); - - return router; -} diff --git a/server/mcp-server.ts b/server/mcp-server.ts index 7dbedb0..7a1177c 100644 --- a/server/mcp-server.ts +++ b/server/mcp-server.ts @@ -2,11 +2,27 @@ import { config } from 'dotenv'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; config({ path: resolve(dirname(fileURLToPath(import.meta.url)), '../.env') }); +import { createBaseMcpServer } from '@corsair/mcp'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { createMcpServer } from './mcp-tools'; +import { corsair } from './corsair'; +import { + cronAdapter, + permissionAdapter, + workflowAdapter, +} from './mcp-adapters'; async function main() { - const server = createMcpServer(); + const basePermissionUrl = + process.env.BASE_PERMISSION_URL ?? + process.env.BASE_URL ?? + 'http://localhost:3000'; + const server = createBaseMcpServer({ + corsair, + workflows: workflowAdapter, + cron: cronAdapter, + permissions: permissionAdapter, + basePermissionUrl, + }); const transport = new StdioServerTransport(); await server.connect(transport); console.error('[corsair-mcp] Server running on stdio'); diff --git a/server/mcp-tools.ts b/server/mcp-tools.ts deleted file mode 100644 index eaad9ce..0000000 --- a/server/mcp-tools.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import { corsair } from './corsair'; -import { - archiveWorkflow, - listWorkflows, - storeWorkflow, - updateWorkflowRecord, -} from './executor'; -import { - registerCronWorkflow, - unregisterCronWorkflow, -} from './workflow-scheduler'; - -const inspect = corsair; - -export function createMcpServer(): McpServer { - const server = new McpServer({ - name: 'corsair', - version: '1.0.0', - description: 'Use this to interact with the Corsair API. Corsair helps you integrate with dozens of tools and services. You can setup cron jobs and webhooks triggered jobs.' - }); - - // ───────────────────────────────────────────────────────────────────────── - // list_operations - // ───────────────────────────────────────────────────────────────────────── - - server.tool( - 'list_operations', - "List available Corsair operations. Without options returns all API endpoints across every plugin. Filter by plugin (e.g. 'slack') and/or type ('api' | 'webhooks' | 'db').", - { - plugin: z.string().optional().describe("Plugin ID to filter by, e.g. 'slack' or 'github'"), - type: z - .enum(['api', 'webhooks', 'db']) - .optional() - .describe("Operation type: 'api' (default), 'webhooks', or 'db'"), - }, - async ({ plugin, type }) => { - const result = inspect.list_operations({ plugin, type }); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], - }; - }, - ); - - // ───────────────────────────────────────────────────────────────────────── - // get_schema - // ───────────────────────────────────────────────────────────────────────── - - server.tool( - 'get_schema', - "Get the schema and metadata for a Corsair operation path. Accepts API paths ('slack.api.channels.list'), webhook paths ('slack.webhooks.messages.message'), or DB paths ('slack.db.messages.search').", - { - path: z - .string() - .describe("Full dot-path from list_operations, e.g. 'slack.api.channels.list'"), - }, - async ({ path }) => { - const result = inspect.get_schema(path); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], - }; - }, - ); - - // ───────────────────────────────────────────────────────────────────────── - // corsair_run - // ───────────────────────────────────────────────────────────────────────── - - server.tool( - 'corsair_run', - "Execute any Corsair API endpoint by its dot-path. Use list_operations to discover paths and get_schema to understand required args. Example path: 'slack.api.channels.list'.", - { - path: z - .string() - .describe("Full API dot-path, e.g. 'slack.api.messages.post'"), - args: z - .record(z.unknown()) - .default({}) - .describe('Arguments object for the operation'), - }, - async ({ path, args }) => { - const parts = path.split('.'); - - if (parts.length < 3) { - return { - isError: true, - content: [ - { - type: 'text', - text: `Invalid path "${path}". Expected format: "plugin.api.group.method". Use list_operations to see valid paths.`, - }, - ], - }; - } - - let fn: unknown = corsair; - for (const part of parts) { - if (typeof fn !== 'object' || fn === null) { - fn = undefined; - break; - } - fn = (fn as Record)[part]; - } - - if (typeof fn !== 'function') { - return { - isError: true, - content: [ - { - type: 'text', - text: `Path "${path}" is not a callable operation. Use list_operations to see valid paths.`, - }, - ], - }; - } - - try { - const result = await (fn as (args: unknown) => Promise)(args); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const extra = err instanceof Error && err.cause ? `\nCause: ${String(err.cause)}` : ''; - const full = JSON.stringify(err, Object.getOwnPropertyNames(err)); - console.error(`[corsair-mcp] corsair_run error for "${path}":`, err); - return { - isError: true, - content: [{ type: 'text', text: `Error running "${path}": ${message}${extra}\n${full}` }], - }; - } - }, - ); - - // ───────────────────────────────────────────────────────────────────────── - // manage_workflows - // Create / list / update / delete webhook and cron workflows. - // ───────────────────────────────────────────────────────────────────────── - - server.tool( - 'manage_workflows', - `List, create, update, or delete Corsair workflows. Use action="create" with webhookTrigger to register a webhook-triggered workflow, or cronSchedule for a cron workflow. - -WORKFLOW CODE CONTRACT: -- The workflow function receives two arguments: ctx and payload. - - ctx.sdk is the full corsair SDK (same as the corsair import), e.g. ctx.sdk.slack.api.messages.post(...) - - payload is the raw webhook event payload (for webhook-triggered workflows) or undefined (for cron/manual). -- Function signature: async function (ctx: { sdk: typeof import('./corsair').corsair }, payload?: unknown) -- Example: - async function myWorkflow(ctx, payload) { - await ctx.sdk.slack.api.messages.post({ channel: 'C12345', text: 'hello' }); - }`, - { - action: z.enum(['list', 'create', 'update', 'delete']).describe('Action to perform'), - triggerType: z - .enum(['cron', 'webhook', 'manual', 'all']) - .optional() - .describe('Filter by trigger type (list only)'), - workflowId: z.string().optional().describe('Workflow function name — required for create/update/delete'), - code: z.string().optional().describe('TypeScript workflow code — required for create'), - description: z.string().optional().describe('Human-readable description'), - cronSchedule: z.string().optional().describe('Cron expression, e.g. "0 9 * * 1-5"'), - webhookTrigger: z - .object({ plugin: z.string(), action: z.string() }) - .optional() - .describe('Webhook trigger, e.g. { plugin: "slack", action: "messages.message" }'), - status: z.enum(['active', 'paused', 'archived']).optional().describe('Workflow status (update only)'), - }, - async ({ action, triggerType, workflowId, code, description, cronSchedule, webhookTrigger, status }) => { - let result: unknown; - - if (action === 'list') { - result = { workflows: await listWorkflows(triggerType) }; - } else if (action === 'create') { - if (!workflowId || !code) { - result = { success: false, error: 'workflowId and code are required for create' }; - } else { - const stored = await storeWorkflow({ - type: 'workflow', - workflowId, - code, - description: description?.trim() || undefined, - cronSchedule: cronSchedule?.trim() || undefined, - webhookTrigger, - }); - if (stored && cronSchedule?.trim()) { - const ok = registerCronWorkflow(stored.id, workflowId, code, cronSchedule.trim()); - if (!ok) { - result = { success: false, error: `Invalid cron expression: "${cronSchedule}"` }; - return { content: [{ type: 'text', text: JSON.stringify(result) }] }; - } - } - result = { success: true, workflow: { id: stored!.id, name: stored!.name, triggerType: stored!.triggerType, status: stored!.status } }; - } - } else if (action === 'delete') { - if (!workflowId) { - result = { success: false, error: 'workflowId is required for delete' }; - } else { - const archived = await archiveWorkflow(workflowId); - if (!archived) { - result = { success: false, error: `Workflow "${workflowId}" not found` }; - } else { - unregisterCronWorkflow(archived.id); - result = { success: true, message: `Workflow "${archived.name}" archived` }; - } - } - } else { - // update - if (!workflowId) { - result = { success: false, error: 'workflowId is required for update' }; - } else { - const updated = await updateWorkflowRecord(workflowId, { code, description, cronSchedule, webhookTrigger, status }); - if (!updated) { - result = { success: false, error: `Workflow "${workflowId}" not found` }; - } else { - if (updated.status === 'archived' || updated.status === 'paused') { - unregisterCronWorkflow(updated.id); - } else if (updated.triggerType === 'cron') { - const cfg = updated.triggerConfig as { cron?: string }; - if (cfg.cron) registerCronWorkflow(updated.id, updated.name, updated.code, cfg.cron); - } - result = { success: true, workflow: { id: updated.id, name: updated.name, triggerType: updated.triggerType, status: updated.status } }; - } - } - } - - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - }, - ); - - return server; -} diff --git a/server/script.ts b/server/script.ts index 1daaa2b..ff43665 100644 --- a/server/script.ts +++ b/server/script.ts @@ -9,7 +9,7 @@ const main = async () => { // console.log(res); - const mcpServer = buildMcpServer({}); + const mcpServer = await buildMcpServer({}); for await (const message of query({ prompt: 'say hi to the user', From c11a069d46fccf9e3c5bad1c4303bd7976f15185 Mon Sep 17 00:00:00 2001 From: Mukul yadav Date: Mon, 9 Mar 2026 16:51:16 +0530 Subject: [PATCH 07/12] feat: integrate corsair permissions handling in MCP server --- server/index.ts | 170 +++++++----------------------------------------- 1 file changed, 22 insertions(+), 148 deletions(-) diff --git a/server/index.ts b/server/index.ts index 5f7d907..d44588b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -3,6 +3,7 @@ import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { processWebhook } from 'corsair'; import { asc, eq } from 'drizzle-orm'; import express from 'express'; +import { corsairPermissions, type PermissionLike } from '@corsair/ui'; import type { SimpleMessage } from './agent'; import { runAgent, WORKFLOW_FAILURE_PROMPT } from './agent'; import { createBaseMcpServer, createMcpRouter } from '@corsair/mcp'; @@ -400,161 +401,34 @@ async function main() { return; } - const FIELD_LABELS: Record> = { - 'messages.post': { - channel: 'Channel', - text: 'Message', - thread_ts: 'Thread', - reply_broadcast: 'Also send to channel', - }, - 'emails.send': { - to: 'To', - from: 'From', - subject: 'Subject', - html: 'Body', - text: 'Body', - cc: 'CC', - bcc: 'BCC', - }, - 'issues.create': { - title: 'Title', - description: 'Description', - teamId: 'Team', - assigneeId: 'Assignee', - priority: 'Priority', - stateId: 'Status', - }, - }; - const PLUGIN_COLORS: Record = { - slack: '#4a154b', - linear: '#5e6ad2', - discord: '#5865f2', - github: '#333', - resend: '#000', - gmail: '#ea4335', + const permissionLike: PermissionLike = { + id: perm.id, + plugin: perm.plugin, + endpoint: perm.operation ?? perm.endpoint, + operation: perm.operation, + description: perm.description, + status: perm.status as PermissionLike['status'], + args: + perm.args && typeof perm.args === 'object' + ? (perm.args as Record) + : {}, + createdAt: perm.createdAt, }; - function esc(s: unknown) { - return String(s ?? '') - .replace(/&/g, '&') - .replace(//g, '>'); - } - function getLabel(op: string, key: string) { - return FIELD_LABELS[op]?.[key] ?? key; - } - function renderArgVal(val: unknown): string { - if (val === null || val === undefined) - return ``; - if (typeof val === 'boolean') return val ? 'Yes' : 'No'; - if (typeof val === 'string') { - if (val.length > 120 || val.includes('\n')) - return `
${esc(val)}
`; - return esc(val); - } - if (Array.isArray(val)) return esc(val.join(', ')); - return `
${esc(JSON.stringify(val, null, 2))}
`; - } - - const isPending = perm.status === 'pending'; - const statusColor = - perm.status === 'granted' || perm.status === 'completed' - ? '#22c55e' - : perm.status === 'declined' - ? '#ef4444' - : '#f59e0b'; - const badgeBg = PLUGIN_COLORS[perm.plugin] ?? '#141414'; - const args = - perm.args && typeof perm.args === 'object' - ? (perm.args as Record) - : {}; - const argEntries = Object.entries(args); - const ts = new Date(perm.createdAt).toLocaleString('en-US', { - dateStyle: 'medium', - timeStyle: 'short', + const onApproval = () => ({ + method: 'POST' as const, + url: `/api/permissions/${id}/resolve`, }); - let statusBar = ''; - if (!isPending) { - const bg = - perm.status === 'granted' || perm.status === 'completed' - ? '#14532d' - : '#7f1d1d'; - statusBar = `
This permission has been ${esc(perm.status)}.
`; - } - - let argsHtml = ''; - if (argEntries.length > 0) { - const rows = argEntries - .map( - ([k, v]) => - `
${esc(getLabel(perm.operation, k))}
${renderArgVal(v)}
`, - ) - .join(''); - argsHtml = `
Request details
${rows}
`; - } + const onDenial = () => ({ + method: 'POST' as const, + url: `/api/permissions/${id}/resolve`, + }); - const actions = isPending - ? `
- - -
- -