diff --git a/package.json b/package.json index 6d0c12d..5d9dcb2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", + "@modelcontextprotocol/sdk": "^1.26.0", "better-sqlite3": "^11.0.0", "cli-highlight": "^2.1.11", "grammy": "^1.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a84b78..202c4b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@anthropic-ai/sdk': specifier: ^0.39.0 version: 0.39.0 + '@modelcontextprotocol/sdk': + specifier: ^1.26.0 + version: 1.26.0(zod@3.25.76) better-sqlite3: specifier: ^11.0.0 version: 11.10.0 @@ -289,6 +292,12 @@ packages: '@grammyjs/types@3.23.0': resolution: {integrity: sha512-D3jQ4UWERPsyR3op/YFudMMIPNTU47vy7L51uO9/73tMELmjO/+LX5N36/Y0CG5IQfIsz43MxiHI5rgsK0/k+g==} + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -308,6 +317,16 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -519,6 +538,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -533,9 +556,20 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -588,12 +622,20 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + 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==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -602,6 +644,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -671,10 +717,30 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + 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'} + convert-to-spaces@2.0.1: resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + 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'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -710,6 +776,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -718,6 +788,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -727,6 +800,10 @@ packages: emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -765,6 +842,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -818,10 +898,22 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + 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'} @@ -830,6 +922,16 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -839,6 +941,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + 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'} @@ -855,6 +960,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -877,6 +986,14 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + 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==} @@ -945,9 +1062,21 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + 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==} @@ -993,6 +1122,14 @@ packages: react-devtools-core: optional: true + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1014,9 +1151,15 @@ packages: engines: {node: '>=20'} hasBin: true + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1030,6 +1173,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1073,14 +1222,30 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + mime-db@1.52.0: 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'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1115,6 +1280,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-abi@3.87.0: resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} engines: {node: '>=10'} @@ -1141,9 +1310,17 @@ packages: 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'} + ollama@0.5.18: resolution: {integrity: sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1188,6 +1365,10 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1200,6 +1381,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1214,6 +1398,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + 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} @@ -1227,6 +1415,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -1234,6 +1426,18 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + 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 @@ -1256,6 +1460,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1272,9 +1480,16 @@ 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'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1283,6 +1498,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1291,6 +1517,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1322,6 +1564,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1403,6 +1649,10 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -1422,6 +1672,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + 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'} @@ -1437,12 +1691,20 @@ packages: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1594,6 +1856,11 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -1745,6 +2012,10 @@ snapshots: '@grammyjs/types@3.23.0': {} + '@hono/node-server@1.19.9(hono@4.11.7)': + dependencies: + hono: 4.11.7 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -1758,6 +2029,28 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + 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.11.7 + 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) + transitivePeerDependencies: + - supports-color + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -1926,6 +2219,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -1936,6 +2234,10 @@ snapshots: dependencies: humanize-ms: 1.2.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -1943,6 +2245,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + 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-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -1986,6 +2295,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + 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.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -1996,6 +2319,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes@3.1.2: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -2003,6 +2328,11 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} chai@5.3.3: @@ -2074,8 +2404,21 @@ snapshots: concat-map@0.0.1: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-to-spaces@2.0.1: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2100,6 +2443,8 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + detect-libc@2.1.2: {} dunder-proto@1.0.1: @@ -2108,12 +2453,16 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ee-first@1.1.1: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} emojilib@2.4.0: {} + encodeurl@2.0.0: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -2170,6 +2519,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -2244,18 +2595,66 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + event-target-shim@5.0.1: {} + 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: {} + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + 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.1 + 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 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2266,6 +2665,17 @@ snapshots: file-uri-to-path@1.0.0: {} + 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 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2293,6 +2703,10 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-constants@1.0.0: {} fsevents@2.3.3: @@ -2360,10 +2774,24 @@ snapshots: highlight.js@10.7.3: {} + hono@4.11.7: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + humanize-ms@1.2.1: dependencies: ms: 2.1.3 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -2420,6 +2848,10 @@ snapshots: - bufferutil - utf-8-validate + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -2434,8 +2866,12 @@ snapshots: is-in-ci@2.0.0: {} + is-promise@4.0.0: {} + isexe@2.0.0: {} + jose@6.1.3: {} + js-tokens@9.0.1: {} js-yaml@4.1.1: @@ -2446,6 +2882,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} keyv@4.5.4: @@ -2486,12 +2926,22 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + 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 + mimic-fn@2.1.0: {} mimic-response@3.1.0: {} @@ -2518,6 +2968,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + node-abi@3.87.0: dependencies: semver: 7.7.3 @@ -2537,10 +2989,16 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + ollama@0.5.18: dependencies: whatwg-fetch: 3.6.20 + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -2593,12 +3051,16 @@ snapshots: parse5@6.0.1: {} + parseurl@1.3.3: {} + patch-console@2.0.0: {} path-exists@4.0.0: {} path-key@3.1.1: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -2607,6 +3069,8 @@ snapshots: picomatch@4.0.3: {} + pkce-challenge@5.0.1: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -2630,6 +3094,11 @@ snapshots: prelude-ls@1.2.1: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -2637,6 +3106,19 @@ snapshots: punycode@2.3.1: {} + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + 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 @@ -2659,6 +3141,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -2699,18 +3183,85 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 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 + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@7.7.3: {} + 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@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -2740,6 +3291,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-width@4.2.3: @@ -2826,6 +3379,8 @@ snapshots: tinyspy@4.0.4: {} + toidentifier@1.0.1: {} + tr46@0.0.3: {} tsx@4.21.0: @@ -2845,6 +3400,12 @@ snapshots: type-fest@4.41.0: {} + 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: {} undici-types@5.26.5: {} @@ -2853,12 +3414,16 @@ snapshots: unicode-emoji-modifier-base@1.0.0: {} + unpipe@1.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 util-deprecate@1.0.2: {} + vary@1.1.2: {} + vite-node@3.2.4(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -2997,4 +3562,8 @@ snapshots: yoga-layout@3.2.1: {} + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} diff --git a/src/config/schema.ts b/src/config/schema.ts index 1bc7760..081d1d6 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -54,6 +54,8 @@ const mcpServerSchema = z.object({ name: z.string(), command: z.string(), args: z.array(z.string()).default([]), + env: z.record(z.string(), z.string()).optional(), + cwd: z.string().optional(), }); const mcpSchema = z.object({ diff --git a/src/daemon/index.ts b/src/daemon/index.ts index dd43de1..3b2a7e1 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -8,6 +8,7 @@ import { ToolRegistry, ToolExecutor, allBuiltinTools } from '../tools/index.js'; import { GatewayServer } from '../gateway/index.js'; import { ChannelRegistry, TelegramAdapter, WebChatAdapter } from '../channels/index.js'; import type { InboundMessage, OutboundMessage } from '../channels/index.js'; +import { McpManager } from '../mcp/index.js'; import { resolve } from 'path'; import { homedir } from 'os'; import { mkdirSync, readFileSync, existsSync } from 'fs'; @@ -23,6 +24,7 @@ export interface DaemonContext { toolExecutor: ToolExecutor; gateway: GatewayServer; channelRegistry: ChannelRegistry; + mcpManager: McpManager; } function loadSystemPrompt(): string { @@ -184,6 +186,19 @@ export async function startDaemon(config: Config): Promise { } const toolExecutor = new ToolExecutor(toolRegistry, hookEngine); + // Initialize MCP manager and start configured servers + const mcpManager = new McpManager(toolRegistry); + + if (config.mcp.servers.length > 0) { + console.log(`Starting ${config.mcp.servers.length} MCP server(s)...`); + await mcpManager.startAll(config.mcp.servers); + } + + lifecycle.onShutdown(async () => { + await mcpManager.stopAll(); + console.log('MCP servers stopped'); + }); + // Initialize model router const modelRouter = createModelRouter(config); @@ -280,6 +295,7 @@ export async function startDaemon(config: Config): Promise { toolExecutor, gateway, channelRegistry, + mcpManager, }; } diff --git a/src/mcp/bridge.test.ts b/src/mcp/bridge.test.ts new file mode 100644 index 0000000..5c6ebdb --- /dev/null +++ b/src/mcp/bridge.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mcpToolName, parseMcpToolName, bridgeMcpTool, bridgeAllTools } from './bridge.js'; +import type { McpClient } from './client.js'; +import type { McpToolInfo } from './types.js'; + +describe('mcpToolName', () => { + it('creates prefixed tool name', () => { + expect(mcpToolName('filesystem', 'read_file')).toBe('mcp:filesystem:read_file'); + }); + + it('handles server names with hyphens', () => { + expect(mcpToolName('my-server', 'do_thing')).toBe('mcp:my-server:do_thing'); + }); +}); + +describe('parseMcpToolName', () => { + it('parses a valid mcp tool name', () => { + expect(parseMcpToolName('mcp:filesystem:read_file')).toEqual({ + serverName: 'filesystem', + toolName: 'read_file', + }); + }); + + it('handles tool names containing colons', () => { + expect(parseMcpToolName('mcp:server:ns:tool')).toEqual({ + serverName: 'server', + toolName: 'ns:tool', + }); + }); + + it('returns null for non-mcp tool names', () => { + expect(parseMcpToolName('shell.exec')).toBeNull(); + expect(parseMcpToolName('file.read')).toBeNull(); + }); + + it('returns null for malformed mcp names', () => { + expect(parseMcpToolName('mcp:')).toBeNull(); + expect(parseMcpToolName('mcp:server')).toBeNull(); + }); +}); + +describe('bridgeMcpTool', () => { + const toolInfo: McpToolInfo = { + name: 'read_file', + description: 'Read a file from disk', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' } }, + required: ['path'], + }, + }; + + function createMockClient( + callResult = { content: 'file contents', isError: false }, + ): McpClient { + return { + serverName: 'filesystem', + tools: [toolInfo], + status: 'connected', + callTool: vi.fn().mockResolvedValue(callResult), + } as unknown as McpClient; + } + + it('creates a tool with prefixed name', () => { + const client = createMockClient(); + const tool = bridgeMcpTool(client, toolInfo); + + expect(tool.name).toBe('mcp:filesystem:read_file'); + }); + + it('includes server name in description', () => { + const client = createMockClient(); + const tool = bridgeMcpTool(client, toolInfo); + + expect(tool.description).toContain('[MCP:filesystem]'); + expect(tool.description).toContain('Read a file from disk'); + }); + + it('preserves input schema', () => { + const client = createMockClient(); + const tool = bridgeMcpTool(client, toolInfo); + + expect(tool.inputSchema.type).toBe('object'); + expect(tool.inputSchema.properties).toEqual({ path: { type: 'string' } }); + expect(tool.inputSchema.required).toEqual(['path']); + }); + + it('execute calls the MCP client with correct tool name and args', async () => { + const client = createMockClient(); + const tool = bridgeMcpTool(client, toolInfo); + + const result = await tool.execute({ path: '/tmp/test.txt' }); + + expect(client.callTool).toHaveBeenCalledWith('read_file', { path: '/tmp/test.txt' }); + expect(result.success).toBe(true); + expect(result.output).toBe('file contents'); + }); + + it('execute returns error result when MCP server reports error', async () => { + const client = createMockClient({ content: 'file not found', isError: true }); + const tool = bridgeMcpTool(client, toolInfo); + + const result = await tool.execute({ path: '/nonexistent' }); + + expect(result.success).toBe(false); + expect(result.error).toBe('file not found'); + }); + + it('execute catches exceptions from MCP client', async () => { + const client = { + serverName: 'filesystem', + tools: [toolInfo], + callTool: vi.fn().mockRejectedValue(new Error('connection lost')), + } as unknown as McpClient; + + const tool = bridgeMcpTool(client, toolInfo); + const result = await tool.execute({ path: '/tmp/test.txt' }); + + expect(result.success).toBe(false); + expect(result.error).toBe('connection lost'); + }); + + it('execute handles null args gracefully', async () => { + const client = createMockClient(); + const tool = bridgeMcpTool(client, toolInfo); + + await tool.execute(null); + + expect(client.callTool).toHaveBeenCalledWith('read_file', {}); + }); +}); + +describe('bridgeAllTools', () => { + it('bridges all tools from a client', () => { + const client = { + serverName: 'myserver', + tools: [ + { name: 'tool_a', description: 'A', inputSchema: { type: 'object' as const, properties: {} } }, + { name: 'tool_b', description: 'B', inputSchema: { type: 'object' as const, properties: {} } }, + ], + callTool: vi.fn(), + } as unknown as McpClient; + + const tools = bridgeAllTools(client); + + expect(tools).toHaveLength(2); + expect(tools[0].name).toBe('mcp:myserver:tool_a'); + expect(tools[1].name).toBe('mcp:myserver:tool_b'); + }); +}); diff --git a/src/mcp/bridge.ts b/src/mcp/bridge.ts new file mode 100644 index 0000000..e98a4ea --- /dev/null +++ b/src/mcp/bridge.ts @@ -0,0 +1,78 @@ +/** + * MCP Bridge — converts MCP tools into Flynn's Tool interface. + * + * Each MCP tool is prefixed with "mcp::" to avoid + * namespace collisions with builtin tools. When the tool is executed, + * the bridge routes the call back to the originating McpClient. + */ + +import type { Tool, ToolResult } from '../tools/types.js'; +import type { McpClient } from './client.js'; +import type { McpToolInfo } from './types.js'; + +/** + * Create the prefixed tool name used in Flynn's tool registry. + * + * Example: MCP server "filesystem" with tool "read_file" -> "mcp:filesystem:read_file" + */ +export function mcpToolName(serverName: string, toolName: string): string { + return `mcp:${serverName}:${toolName}`; +} + +/** + * Parse a prefixed tool name back into server + tool components. + * Returns null if the name doesn't match the mcp: prefix pattern. + */ +export function parseMcpToolName(prefixedName: string): { serverName: string; toolName: string } | null { + const match = prefixedName.match(/^mcp:([^:]+):(.+)$/); + if (!match) return null; + return { serverName: match[1], toolName: match[2] }; +} + +/** + * Convert a single MCP tool into a Flynn Tool. + * + * The returned Tool's execute() calls back into the McpClient + * to invoke the tool on the remote MCP server. + */ +export function bridgeMcpTool(client: McpClient, toolInfo: McpToolInfo): Tool { + const prefixedName = mcpToolName(client.serverName, toolInfo.name); + + return { + name: prefixedName, + description: `[MCP:${client.serverName}] ${toolInfo.description}`, + inputSchema: { + type: 'object', + properties: toolInfo.inputSchema.properties ?? {}, + required: toolInfo.inputSchema.required, + }, + + async execute(args: unknown): Promise { + try { + const result = await client.callTool( + toolInfo.name, + (args ?? {}) as Record, + ); + + return { + success: !result.isError, + output: result.content, + error: result.isError ? result.content : undefined, + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, + }; +} + +/** + * Bridge all tools from an MCP client into Flynn Tool objects. + */ +export function bridgeAllTools(client: McpClient): Tool[] { + return client.tools.map((t) => bridgeMcpTool(client, t)); +} diff --git a/src/mcp/client.ts b/src/mcp/client.ts new file mode 100644 index 0000000..e1ed66f --- /dev/null +++ b/src/mcp/client.ts @@ -0,0 +1,170 @@ +/** + * MCP Client — wraps the MCP SDK to manage a single server connection. + * + * Handles stdio transport lifecycle, tool discovery, and tool invocation. + * Each McpClient instance maps to one MCP server process. + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +import type { McpServerConfig, McpToolInfo, McpServerStatus } from './types.js'; + +export class McpClient { + readonly serverName: string; + + private client: Client | null = null; + private transport: StdioClientTransport | null = null; + private _status: McpServerStatus = 'disconnected'; + private _tools: McpToolInfo[] = []; + private _error?: string; + private config: McpServerConfig; + + get status(): McpServerStatus { + return this._status; + } + + get tools(): McpToolInfo[] { + return this._tools; + } + + get error(): string | undefined { + return this._error; + } + + constructor(config: McpServerConfig) { + this.config = config; + this.serverName = config.name; + } + + /** + * Connect to the MCP server: spawn process, initialize protocol, discover tools. + */ + async connect(): Promise { + if (this._status === 'connected' || this._status === 'connecting') { + return; + } + + this._status = 'connecting'; + this._error = undefined; + + try { + // Create stdio transport — spawns the server process + this.transport = new StdioClientTransport({ + command: this.config.command, + args: this.config.args, + env: this.config.env, + cwd: this.config.cwd, + stderr: 'pipe', + }); + + // Create MCP client + this.client = new Client( + { name: 'flynn', version: '0.1.0' }, + { capabilities: {} }, + ); + + // Connect — performs MCP initialize handshake + await this.client.connect(this.transport); + + // Discover tools + await this.refreshTools(); + + this._status = 'connected'; + console.log(`MCP server '${this.serverName}' connected (${this._tools.length} tools)`); + } catch (error) { + this._status = 'error'; + this._error = error instanceof Error ? error.message : String(error); + console.error(`MCP server '${this.serverName}' failed to connect:`, this._error); + + // Clean up partial state + await this.cleanup(); + throw error; + } + } + + /** + * Disconnect from the MCP server and kill the process. + */ + async disconnect(): Promise { + await this.cleanup(); + this._status = 'disconnected'; + this._tools = []; + this._error = undefined; + } + + /** + * Re-discover tools from the server (e.g. after a tools/list_changed notification). + */ + async refreshTools(): Promise { + if (!this.client) { + throw new Error(`MCP server '${this.serverName}' is not connected`); + } + + const result = await this.client.listTools(); + + this._tools = result.tools.map((t) => ({ + name: t.name, + description: t.description ?? '', + inputSchema: { + type: 'object' as const, + properties: (t.inputSchema.properties ?? {}) as Record, + required: t.inputSchema.required, + }, + })); + + return this._tools; + } + + /** + * Call a tool on this MCP server by its original (unprefixed) name. + * + * Returns the text content of the result, or throws on error. + */ + async callTool(toolName: string, args: Record): Promise<{ content: string; isError: boolean }> { + if (!this.client) { + throw new Error(`MCP server '${this.serverName}' is not connected`); + } + + const result = await this.client.callTool({ name: toolName, arguments: args }); + + // Extract text content from the result + const textParts: string[] = []; + let isError = false; + + if ('isError' in result && result.isError) { + isError = true; + } + + if ('content' in result && Array.isArray(result.content)) { + for (const block of result.content) { + if ('text' in block && typeof block.text === 'string') { + textParts.push(block.text); + } + } + } + + // Fallback: if toolResult format is returned instead + if ('toolResult' in result && textParts.length === 0) { + textParts.push(String(result.toolResult)); + } + + return { + content: textParts.join('\n') || '(no output)', + isError, + }; + } + + /** Clean up transport and client without changing status. */ + private async cleanup(): Promise { + try { + if (this.transport) { + await this.transport.close(); + } + } catch { + // Ignore close errors + } + this.client = null; + this.transport = null; + } +} diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 0000000..8fb1e35 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,4 @@ +export type { McpServerConfig, McpToolInfo, McpServerState, McpServerStatus } from './types.js'; +export { McpClient } from './client.js'; +export { McpManager } from './manager.js'; +export { bridgeMcpTool, bridgeAllTools, mcpToolName, parseMcpToolName } from './bridge.js'; diff --git a/src/mcp/manager.test.ts b/src/mcp/manager.test.ts new file mode 100644 index 0000000..364dccc --- /dev/null +++ b/src/mcp/manager.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { McpManager } from './manager.js'; +import { ToolRegistry } from '../tools/registry.js'; + +// Mock McpClient to avoid spawning real processes +vi.mock('./client.js', () => ({ + McpClient: vi.fn().mockImplementation((config) => ({ + serverName: config.name, + status: 'disconnected', + tools: [], + error: undefined, + connect: vi.fn(async function (this: { status: string; tools: { name: string; description: string; inputSchema: { type: string; properties: Record } }[] }) { + this.status = 'connected'; + // Simulate tool discovery + this.tools = [ + { + name: 'do_thing', + description: 'Does a thing', + inputSchema: { type: 'object', properties: { input: { type: 'string' } } }, + }, + ]; + }), + disconnect: vi.fn(async function (this: { status: string; tools: unknown[] }) { + this.status = 'disconnected'; + this.tools = []; + }), + callTool: vi.fn().mockResolvedValue({ content: 'result', isError: false }), + })), +})); + +describe('McpManager', () => { + let registry: ToolRegistry; + let manager: McpManager; + + beforeEach(() => { + vi.clearAllMocks(); + registry = new ToolRegistry(); + manager = new McpManager(registry); + }); + + it('starts a server and registers its tools', async () => { + await manager.startServer({ + name: 'test-server', + command: 'test-cmd', + args: [], + }); + + // Tool should be registered with mcp: prefix + const tool = registry.get('mcp:test-server:do_thing'); + expect(tool).toBeDefined(); + expect(tool!.name).toBe('mcp:test-server:do_thing'); + expect(tool!.description).toContain('[MCP:test-server]'); + }); + + it('startAll handles multiple servers', async () => { + await manager.startAll([ + { name: 'server-a', command: 'cmd-a', args: [] }, + { name: 'server-b', command: 'cmd-b', args: [] }, + ]); + + expect(registry.get('mcp:server-a:do_thing')).toBeDefined(); + expect(registry.get('mcp:server-b:do_thing')).toBeDefined(); + }); + + it('stopServer unregisters tools and disconnects', async () => { + await manager.startServer({ + name: 'test-server', + command: 'test-cmd', + args: [], + }); + + expect(registry.get('mcp:test-server:do_thing')).toBeDefined(); + + await manager.stopServer('test-server'); + + expect(registry.get('mcp:test-server:do_thing')).toBeUndefined(); + }); + + it('stopAll stops all servers', async () => { + await manager.startAll([ + { name: 'server-a', command: 'cmd-a', args: [] }, + { name: 'server-b', command: 'cmd-b', args: [] }, + ]); + + await manager.stopAll(); + + expect(registry.get('mcp:server-a:do_thing')).toBeUndefined(); + expect(registry.get('mcp:server-b:do_thing')).toBeUndefined(); + }); + + it('stopServer is safe when server does not exist', async () => { + await expect(manager.stopServer('nonexistent')).resolves.toBeUndefined(); + }); + + it('startServer replaces existing server with same name', async () => { + await manager.startServer({ + name: 'test-server', + command: 'cmd-v1', + args: [], + }); + + // Start again with same name — should replace + await manager.startServer({ + name: 'test-server', + command: 'cmd-v2', + args: [], + }); + + // Tool should still be registered (re-registered) + expect(registry.get('mcp:test-server:do_thing')).toBeDefined(); + }); + + it('listServers returns state for all servers', async () => { + await manager.startAll([ + { name: 'server-a', command: 'cmd-a', args: [] }, + { name: 'server-b', command: 'cmd-b', args: [] }, + ]); + + const servers = manager.listServers(); + expect(servers).toHaveLength(2); + expect(servers[0].config.name).toBe('server-a'); + expect(servers[1].config.name).toBe('server-b'); + }); + + it('getServerState returns undefined for unknown server', () => { + expect(manager.getServerState('nonexistent')).toBeUndefined(); + }); + + it('getServerState returns state for known server', async () => { + await manager.startServer({ + name: 'test-server', + command: 'test-cmd', + args: ['--flag'], + }); + + const state = manager.getServerState('test-server'); + expect(state).toBeDefined(); + expect(state!.config.name).toBe('test-server'); + expect(state!.config.args).toEqual(['--flag']); + expect(state!.tools).toHaveLength(1); + }); + + it('getRegisteredTools returns all MCP tools', async () => { + await manager.startAll([ + { name: 'server-a', command: 'cmd-a', args: [] }, + { name: 'server-b', command: 'cmd-b', args: [] }, + ]); + + const tools = manager.getRegisteredTools(); + expect(tools).toHaveLength(2); + expect(tools.map((t) => t.name)).toContain('mcp:server-a:do_thing'); + expect(tools.map((t) => t.name)).toContain('mcp:server-b:do_thing'); + }); + + it('restartServer stops and restarts with same config', async () => { + await manager.startServer({ + name: 'test-server', + command: 'test-cmd', + args: ['--arg1'], + }); + + await manager.restartServer('test-server'); + + const state = manager.getServerState('test-server'); + expect(state).toBeDefined(); + expect(state!.config.args).toEqual(['--arg1']); + expect(registry.get('mcp:test-server:do_thing')).toBeDefined(); + }); + + it('restartServer throws for unknown server', async () => { + await expect(manager.restartServer('nonexistent')).rejects.toThrow( + "MCP server 'nonexistent' not found", + ); + }); +}); diff --git a/src/mcp/manager.ts b/src/mcp/manager.ts new file mode 100644 index 0000000..57a2025 --- /dev/null +++ b/src/mcp/manager.ts @@ -0,0 +1,176 @@ +/** + * MCP Manager — lifecycle management for MCP server connections. + * + * Reads server configs, creates McpClients, connects them, bridges + * their tools into the Flynn ToolRegistry, and handles shutdown. + */ + +import type { ToolRegistry } from '../tools/registry.js'; +import type { Tool } from '../tools/types.js'; +import type { McpServerConfig, McpServerState } from './types.js'; +import { McpClient } from './client.js'; +import { bridgeAllTools } from './bridge.js'; + +export class McpManager { + private clients: Map = new Map(); + private registeredToolNames: Map = new Map(); // serverName -> prefixed tool names + private toolRegistry: ToolRegistry; + + constructor(toolRegistry: ToolRegistry) { + this.toolRegistry = toolRegistry; + } + + /** + * Start all MCP servers from config and register their tools. + * + * Errors on individual servers are caught and logged — one bad server + * doesn't prevent the others from starting. + */ + async startAll(configs: McpServerConfig[]): Promise { + const results = await Promise.allSettled( + configs.map((config) => this.startServer(config)), + ); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === 'rejected') { + console.error( + `MCP server '${configs[i].name}' failed to start:`, + result.reason instanceof Error ? result.reason.message : result.reason, + ); + } + } + } + + /** + * Start a single MCP server, connect, discover tools, and register them. + */ + async startServer(config: McpServerConfig): Promise { + // Stop existing server with same name if any + if (this.clients.has(config.name)) { + await this.stopServer(config.name); + } + + const client = new McpClient(config); + this.clients.set(config.name, client); + this.storedConfigs.set(config.name, config); + + // Connect and discover tools + await client.connect(); + + // Bridge discovered tools into Flynn's registry + const tools = bridgeAllTools(client); + const toolNames: string[] = []; + + for (const tool of tools) { + try { + this.toolRegistry.register(tool); + toolNames.push(tool.name); + } catch (error) { + console.warn( + `Could not register MCP tool '${tool.name}':`, + error instanceof Error ? error.message : error, + ); + } + } + + this.registeredToolNames.set(config.name, toolNames); + + console.log( + `MCP server '${config.name}': ${toolNames.length} tools registered`, + ); + } + + /** + * Stop a single MCP server and unregister its tools. + */ + async stopServer(name: string): Promise { + const client = this.clients.get(name); + if (!client) return; + + // Unregister tools from the registry + const toolNames = this.registeredToolNames.get(name) ?? []; + for (const toolName of toolNames) { + this.toolRegistry.unregister(toolName); + } + this.registeredToolNames.delete(name); + + // Disconnect client + await client.disconnect(); + this.clients.delete(name); + this.storedConfigs.delete(name); + } + + /** + * Stop all MCP servers and clean up. + */ + async stopAll(): Promise { + const names = Array.from(this.clients.keys()); + await Promise.allSettled(names.map((name) => this.stopServer(name))); + } + + /** + * Restart a single MCP server (stop + start with same config). + */ + async restartServer(name: string): Promise { + const client = this.clients.get(name); + if (!client) { + throw new Error(`MCP server '${name}' not found`); + } + + // We need the original config to restart + const state = this.getServerState(name); + if (!state) { + throw new Error(`MCP server '${name}' state not found`); + } + + await this.stopServer(name); + await this.startServer(state.config); + } + + /** + * Get the state of a specific server. + */ + getServerState(name: string): McpServerState | undefined { + const client = this.clients.get(name); + if (!client) return undefined; + + const config = this.storedConfigs.get(name) ?? { name, command: '', args: [] }; + + return { + config, + status: client.status, + tools: client.tools, + error: client.error, + }; + } + + /** + * List all MCP server states. + */ + listServers(): McpServerState[] { + return Array.from(this.clients.entries()).map(([name, client]) => ({ + config: this.storedConfigs.get(name) ?? { name, command: '', args: [] }, + status: client.status, + tools: client.tools, + error: client.error, + })); + } + + /** + * Get all registered MCP tools (as Flynn Tool objects). + */ + getRegisteredTools(): Tool[] { + const tools: Tool[] = []; + for (const toolNames of this.registeredToolNames.values()) { + for (const name of toolNames) { + const tool = this.toolRegistry.get(name); + if (tool) tools.push(tool); + } + } + return tools; + } + + /** Stored configs for restart support and state reporting. */ + private storedConfigs: Map = new Map(); +} diff --git a/src/mcp/types.ts b/src/mcp/types.ts new file mode 100644 index 0000000..a2385eb --- /dev/null +++ b/src/mcp/types.ts @@ -0,0 +1,49 @@ +/** + * MCP (Model Context Protocol) type definitions. + * + * Internal types for Flynn's MCP client integration. + * These wrap the SDK types into simpler forms used by the bridge and manager. + */ + +/** Configuration for a single MCP server from flynn.yaml. */ +export interface McpServerConfig { + /** Unique name for this server (used as tool name prefix). */ + name: string; + /** Executable command to spawn the server process. */ + command: string; + /** Arguments passed to the command. */ + args: string[]; + /** Optional environment variables for the server process. */ + env?: Record; + /** Optional working directory for the server process. */ + cwd?: string; +} + +/** Connection status of an MCP server. */ +export type McpServerStatus = 'disconnected' | 'connecting' | 'connected' | 'error'; + +/** Information about a tool discovered from an MCP server. */ +export interface McpToolInfo { + /** Tool name as reported by the MCP server. */ + name: string; + /** Human-readable description. */ + description: string; + /** JSON Schema for the tool's input parameters. */ + inputSchema: { + type: 'object'; + properties?: Record; + required?: string[]; + }; +} + +/** Runtime state of a connected MCP server. */ +export interface McpServerState { + /** Config this server was started from. */ + config: McpServerConfig; + /** Current connection status. */ + status: McpServerStatus; + /** Tools discovered from this server. */ + tools: McpToolInfo[]; + /** Error message if status is 'error'. */ + error?: string; +} diff --git a/src/tools/registry.test.ts b/src/tools/registry.test.ts index 7ddc3c2..b27204c 100644 --- a/src/tools/registry.test.ts +++ b/src/tools/registry.test.ts @@ -62,6 +62,22 @@ describe('ToolRegistry', () => { }]); }); + it('unregisters a tool by name', () => { + const registry = new ToolRegistry(); + registry.register(echoTool); + registry.register(greetTool); + + expect(registry.unregister('test.echo')).toBe(true); + expect(registry.get('test.echo')).toBeUndefined(); + expect(registry.list()).toHaveLength(1); + expect(registry.list()[0].name).toBe('test.greet'); + }); + + it('returns false when unregistering a nonexistent tool', () => { + const registry = new ToolRegistry(); + expect(registry.unregister('nonexistent')).toBe(false); + }); + it('serializes to OpenAI format', () => { const registry = new ToolRegistry(); registry.register(echoTool); diff --git a/src/tools/registry.ts b/src/tools/registry.ts index 03d395a..541ae03 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -25,6 +25,10 @@ export class ToolRegistry { this.tools.set(tool.name, tool); } + unregister(name: string): boolean { + return this.tools.delete(name); + } + get(name: string): Tool | undefined { return this.tools.get(name); }