Compare commits
8 Commits
feat/datab
...
feat/docke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1390313a3 | ||
|
|
0ff5c85cf6 | ||
|
|
3fa2bc5ffa | ||
|
|
47f10f62c7 | ||
|
|
247b4967e5 | ||
|
|
dc45f5981b | ||
| f5fae2936a | |||
|
|
386029d052 |
@@ -15,6 +15,35 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- mcpctl
|
||||||
|
|
||||||
|
mcpd:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.mcpd
|
||||||
|
container_name: mcpctl-mcpd
|
||||||
|
ports:
|
||||||
|
- "3100:3100"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://mcpctl:mcpctl_dev@postgres:5432/mcpctl
|
||||||
|
PORT: "3100"
|
||||||
|
HOST: "0.0.0.0"
|
||||||
|
LOG_LEVEL: info
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
networks:
|
||||||
|
- mcpctl
|
||||||
|
- mcp-servers
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:3100/healthz || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
postgres-test:
|
postgres-test:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
@@ -32,6 +61,15 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
profiles:
|
||||||
|
- test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mcpctl:
|
||||||
|
driver: bridge
|
||||||
|
mcp-servers:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mcpctl-pgdata:
|
mcpctl-pgdata:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.0",
|
"packageManager": "pnpm@9.15.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||||
"@typescript-eslint/parser": "^8.56.0",
|
"@typescript-eslint/parser": "^8.56.0",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
|
|||||||
434
pnpm-lock.yaml
generated
434
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^25.3.0
|
||||||
|
version: 25.3.0
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^8.56.0
|
specifier: ^8.56.0
|
||||||
version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)
|
||||||
@@ -109,6 +112,9 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)
|
version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)
|
||||||
|
dockerode:
|
||||||
|
specifier: ^4.0.9
|
||||||
|
version: 4.0.9
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.7.4
|
version: 5.7.4
|
||||||
@@ -116,6 +122,9 @@ importers:
|
|||||||
specifier: ^3.24.0
|
specifier: ^3.24.0
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/dockerode':
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.3.0
|
specifier: ^25.3.0
|
||||||
version: 25.3.0
|
version: 25.3.0
|
||||||
@@ -145,6 +154,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@balena/dockerignore@1.0.2':
|
||||||
|
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2':
|
'@bcoe/v8-coverage@1.0.2':
|
||||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -362,6 +374,20 @@ packages:
|
|||||||
'@fastify/rate-limit@10.3.0':
|
'@fastify/rate-limit@10.3.0':
|
||||||
resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==}
|
resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==}
|
||||||
|
|
||||||
|
'@grpc/grpc-js@1.14.3':
|
||||||
|
resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
|
||||||
|
engines: {node: '>=12.10.0'}
|
||||||
|
|
||||||
|
'@grpc/proto-loader@0.7.15':
|
||||||
|
resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@grpc/proto-loader@0.8.0':
|
||||||
|
resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@hono/node-server@1.19.9':
|
'@hono/node-server@1.19.9':
|
||||||
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
@@ -528,6 +554,9 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@js-sdsl/ordered-map@4.4.2':
|
||||||
|
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
||||||
|
|
||||||
'@lukeed/ms@2.0.2':
|
'@lukeed/ms@2.0.2':
|
||||||
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -575,6 +604,36 @@ packages:
|
|||||||
'@prisma/get-platform@6.19.2':
|
'@prisma/get-platform@6.19.2':
|
||||||
resolution: {integrity: sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==}
|
resolution: {integrity: sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==}
|
||||||
|
|
||||||
|
'@protobufjs/aspromise@1.1.2':
|
||||||
|
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||||
|
|
||||||
|
'@protobufjs/base64@1.1.2':
|
||||||
|
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
|
||||||
|
|
||||||
|
'@protobufjs/codegen@2.0.4':
|
||||||
|
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
|
||||||
|
|
||||||
|
'@protobufjs/eventemitter@1.1.0':
|
||||||
|
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
|
||||||
|
|
||||||
|
'@protobufjs/fetch@1.1.0':
|
||||||
|
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
|
||||||
|
|
||||||
|
'@protobufjs/float@1.0.2':
|
||||||
|
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
|
||||||
|
|
||||||
|
'@protobufjs/inquire@1.1.0':
|
||||||
|
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
|
||||||
|
|
||||||
|
'@protobufjs/path@1.1.2':
|
||||||
|
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
|
||||||
|
|
||||||
|
'@protobufjs/pool@1.1.0':
|
||||||
|
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
|
||||||
|
|
||||||
|
'@protobufjs/utf8@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.58.0':
|
'@rollup/rollup-android-arm-eabi@4.58.0':
|
||||||
resolution: {integrity: sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==}
|
resolution: {integrity: sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@@ -709,6 +768,12 @@ packages:
|
|||||||
'@types/deep-eql@4.0.2':
|
'@types/deep-eql@4.0.2':
|
||||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
|
'@types/docker-modem@3.0.6':
|
||||||
|
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
|
||||||
|
|
||||||
|
'@types/dockerode@4.0.1':
|
||||||
|
resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==}
|
||||||
|
|
||||||
'@types/esrecurse@4.3.1':
|
'@types/esrecurse@4.3.1':
|
||||||
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||||
|
|
||||||
@@ -721,9 +786,15 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/node@18.19.130':
|
||||||
|
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||||
|
|
||||||
'@types/node@25.3.0':
|
'@types/node@25.3.0':
|
||||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||||
|
|
||||||
|
'@types/ssh2@1.15.5':
|
||||||
|
resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.56.0':
|
'@typescript-eslint/eslint-plugin@8.56.0':
|
||||||
resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==}
|
resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -863,6 +934,9 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
asn1@0.2.6:
|
||||||
|
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
|
||||||
|
|
||||||
assertion-error@2.0.1:
|
assertion-error@2.0.1:
|
||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -884,6 +958,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
|
resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
base64-js@1.5.1:
|
||||||
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
|
bcrypt-pbkdf@1.0.2:
|
||||||
|
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -895,6 +978,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
|
buildcheck@0.0.7:
|
||||||
|
resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -930,6 +1020,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
|
chownr@1.1.4:
|
||||||
|
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||||
|
|
||||||
citty@0.1.6:
|
citty@0.1.6:
|
||||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||||
|
|
||||||
@@ -940,6 +1033,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -982,6 +1079,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
cpu-features@0.0.10:
|
||||||
|
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1016,6 +1117,14 @@ packages:
|
|||||||
destr@2.0.5:
|
destr@2.0.5:
|
||||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||||
|
|
||||||
|
docker-modem@5.0.6:
|
||||||
|
resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==}
|
||||||
|
engines: {node: '>= 8.0'}
|
||||||
|
|
||||||
|
dockerode@4.0.9:
|
||||||
|
resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==}
|
||||||
|
engines: {node: '>= 8.0'}
|
||||||
|
|
||||||
dotenv@16.6.1:
|
dotenv@16.6.1:
|
||||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1041,6 +1150,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
es-define-property@1.0.1:
|
es-define-property@1.0.1:
|
||||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1061,6 +1173,10 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
escalade@3.2.0:
|
||||||
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
escape-html@1.0.3:
|
escape-html@1.0.3:
|
||||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
@@ -1222,6 +1338,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
fs-constants@1.0.0:
|
||||||
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -1230,6 +1349,10 @@ packages:
|
|||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1288,6 +1411,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -1400,6 +1526,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
lodash.camelcase@4.3.0:
|
||||||
|
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||||
|
|
||||||
|
long@5.3.2:
|
||||||
|
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||||
|
|
||||||
lru-cache@11.2.6:
|
lru-cache@11.2.6:
|
||||||
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -1446,6 +1578,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3:
|
||||||
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
mnemonist@0.40.0:
|
mnemonist@0.40.0:
|
||||||
resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==}
|
resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==}
|
||||||
|
|
||||||
@@ -1456,6 +1591,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
|
||||||
|
nan@2.25.0:
|
||||||
|
resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==}
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -1592,10 +1730,17 @@ packages:
|
|||||||
process-warning@5.0.0:
|
process-warning@5.0.0:
|
||||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||||
|
|
||||||
|
protobufjs@7.5.4:
|
||||||
|
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1621,6 +1766,10 @@ packages:
|
|||||||
rc9@2.1.2:
|
rc9@2.1.2:
|
||||||
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
|
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
@@ -1629,6 +1778,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||||
engines: {node: '>= 12.13.0'}
|
engines: {node: '>= 12.13.0'}
|
||||||
|
|
||||||
|
require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
require-from-string@2.0.2:
|
require-from-string@2.0.2:
|
||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1668,6 +1821,9 @@ packages:
|
|||||||
rxjs@7.8.2:
|
rxjs@7.8.2:
|
||||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
safe-regex2@5.0.0:
|
safe-regex2@5.0.0:
|
||||||
resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
|
resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
|
||||||
|
|
||||||
@@ -1738,10 +1894,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
split-ca@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
|
||||||
|
|
||||||
split2@4.2.0:
|
split2@4.2.0:
|
||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
engines: {node: '>= 10.x'}
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
|
ssh2@1.17.0:
|
||||||
|
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
|
||||||
|
engines: {node: '>=10.16.0'}
|
||||||
|
|
||||||
stackback@0.0.2:
|
stackback@0.0.2:
|
||||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
@@ -1756,6 +1919,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1764,6 +1930,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
thread-stream@4.0.0:
|
thread-stream@4.0.0:
|
||||||
resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
|
resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -1805,6 +1978,9 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tweetnacl@0.14.5:
|
||||||
|
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1818,6 +1994,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@5.26.5:
|
||||||
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
undici-types@7.18.2:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
@@ -1828,6 +2007,13 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
uuid@10.0.0:
|
||||||
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1924,9 +2110,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
y18n@5.0.8:
|
||||||
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yocto-queue@0.1.0:
|
yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1958,6 +2160,8 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@balena/dockerignore@1.0.2': {}
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.3':
|
'@esbuild/aix-ppc64@0.27.3':
|
||||||
@@ -2107,6 +2311,25 @@ snapshots:
|
|||||||
fastify-plugin: 5.1.0
|
fastify-plugin: 5.1.0
|
||||||
toad-cache: 3.7.0
|
toad-cache: 3.7.0
|
||||||
|
|
||||||
|
'@grpc/grpc-js@1.14.3':
|
||||||
|
dependencies:
|
||||||
|
'@grpc/proto-loader': 0.8.0
|
||||||
|
'@js-sdsl/ordered-map': 4.4.2
|
||||||
|
|
||||||
|
'@grpc/proto-loader@0.7.15':
|
||||||
|
dependencies:
|
||||||
|
lodash.camelcase: 4.3.0
|
||||||
|
long: 5.3.2
|
||||||
|
protobufjs: 7.5.4
|
||||||
|
yargs: 17.7.2
|
||||||
|
|
||||||
|
'@grpc/proto-loader@0.8.0':
|
||||||
|
dependencies:
|
||||||
|
lodash.camelcase: 4.3.0
|
||||||
|
long: 5.3.2
|
||||||
|
protobufjs: 7.5.4
|
||||||
|
yargs: 17.7.2
|
||||||
|
|
||||||
'@hono/node-server@1.19.9(hono@4.12.0)':
|
'@hono/node-server@1.19.9(hono@4.12.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.0
|
hono: 4.12.0
|
||||||
@@ -2256,6 +2479,8 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@js-sdsl/ordered-map@4.4.2': {}
|
||||||
|
|
||||||
'@lukeed/ms@2.0.2': {}
|
'@lukeed/ms@2.0.2': {}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)':
|
'@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)':
|
||||||
@@ -2317,6 +2542,29 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/debug': 6.19.2
|
'@prisma/debug': 6.19.2
|
||||||
|
|
||||||
|
'@protobufjs/aspromise@1.1.2': {}
|
||||||
|
|
||||||
|
'@protobufjs/base64@1.1.2': {}
|
||||||
|
|
||||||
|
'@protobufjs/codegen@2.0.4': {}
|
||||||
|
|
||||||
|
'@protobufjs/eventemitter@1.1.0': {}
|
||||||
|
|
||||||
|
'@protobufjs/fetch@1.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@protobufjs/aspromise': 1.1.2
|
||||||
|
'@protobufjs/inquire': 1.1.0
|
||||||
|
|
||||||
|
'@protobufjs/float@1.0.2': {}
|
||||||
|
|
||||||
|
'@protobufjs/inquire@1.1.0': {}
|
||||||
|
|
||||||
|
'@protobufjs/path@1.1.2': {}
|
||||||
|
|
||||||
|
'@protobufjs/pool@1.1.0': {}
|
||||||
|
|
||||||
|
'@protobufjs/utf8@1.1.0': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.58.0':
|
'@rollup/rollup-android-arm-eabi@4.58.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2401,6 +2649,17 @@ snapshots:
|
|||||||
|
|
||||||
'@types/deep-eql@4.0.2': {}
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/docker-modem@3.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.3.0
|
||||||
|
'@types/ssh2': 1.15.5
|
||||||
|
|
||||||
|
'@types/dockerode@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/docker-modem': 3.0.6
|
||||||
|
'@types/node': 25.3.0
|
||||||
|
'@types/ssh2': 1.15.5
|
||||||
|
|
||||||
'@types/esrecurse@4.3.1': {}
|
'@types/esrecurse@4.3.1': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
@@ -2409,10 +2668,18 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/node@18.19.130':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 5.26.5
|
||||||
|
|
||||||
'@types/node@25.3.0':
|
'@types/node@25.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
|
'@types/ssh2@1.15.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.130
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -2596,6 +2863,10 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
|
asn1@0.2.6:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
ast-v8-to-istanbul@0.3.11:
|
ast-v8-to-istanbul@0.3.11:
|
||||||
@@ -2615,6 +2886,18 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@4.0.3: {}
|
balanced-match@4.0.3: {}
|
||||||
|
|
||||||
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
|
bcrypt-pbkdf@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
tweetnacl: 0.14.5
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -2637,6 +2920,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.3
|
balanced-match: 4.0.3
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
buildcheck@0.0.7:
|
||||||
|
optional: true
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
c12@3.1.0:
|
c12@3.1.0:
|
||||||
@@ -2674,6 +2965,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
|
chownr@1.1.4: {}
|
||||||
|
|
||||||
citty@0.1.6:
|
citty@0.1.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
@@ -2682,6 +2975,12 @@ snapshots:
|
|||||||
|
|
||||||
cli-width@4.1.0: {}
|
cli-width@4.1.0: {}
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -2709,6 +3008,12 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
|
cpu-features@0.0.10:
|
||||||
|
dependencies:
|
||||||
|
buildcheck: 0.0.7
|
||||||
|
nan: 2.25.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -2731,6 +3036,27 @@ snapshots:
|
|||||||
|
|
||||||
destr@2.0.5: {}
|
destr@2.0.5: {}
|
||||||
|
|
||||||
|
docker-modem@5.0.6:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
split-ca: 1.0.1
|
||||||
|
ssh2: 1.17.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
dockerode@4.0.9:
|
||||||
|
dependencies:
|
||||||
|
'@balena/dockerignore': 1.0.2
|
||||||
|
'@grpc/grpc-js': 1.14.3
|
||||||
|
'@grpc/proto-loader': 0.7.15
|
||||||
|
docker-modem: 5.0.6
|
||||||
|
protobufjs: 7.5.4
|
||||||
|
tar-fs: 2.1.4
|
||||||
|
uuid: 10.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
dotenv@16.6.1: {}
|
dotenv@16.6.1: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@@ -2752,6 +3078,10 @@ snapshots:
|
|||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
@@ -2791,6 +3121,8 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.27.3
|
'@esbuild/win32-ia32': 0.27.3
|
||||||
'@esbuild/win32-x64': 0.27.3
|
'@esbuild/win32-x64': 0.27.3
|
||||||
|
|
||||||
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-html@1.0.3: {}
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
@@ -3011,11 +3343,15 @@ snapshots:
|
|||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -3085,6 +3421,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
@@ -3179,6 +3517,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
||||||
|
lodash.camelcase@4.3.0: {}
|
||||||
|
|
||||||
|
long@5.3.2: {}
|
||||||
|
|
||||||
lru-cache@11.2.6: {}
|
lru-cache@11.2.6: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
@@ -3217,6 +3559,8 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.3: {}
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3: {}
|
||||||
|
|
||||||
mnemonist@0.40.0:
|
mnemonist@0.40.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
obliterator: 2.0.5
|
obliterator: 2.0.5
|
||||||
@@ -3225,6 +3569,9 @@ snapshots:
|
|||||||
|
|
||||||
mute-stream@2.0.0: {}
|
mute-stream@2.0.0: {}
|
||||||
|
|
||||||
|
nan@2.25.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
@@ -3348,11 +3695,31 @@ snapshots:
|
|||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
|
|
||||||
|
protobufjs@7.5.4:
|
||||||
|
dependencies:
|
||||||
|
'@protobufjs/aspromise': 1.1.2
|
||||||
|
'@protobufjs/base64': 1.1.2
|
||||||
|
'@protobufjs/codegen': 2.0.4
|
||||||
|
'@protobufjs/eventemitter': 1.1.0
|
||||||
|
'@protobufjs/fetch': 1.1.0
|
||||||
|
'@protobufjs/float': 1.0.2
|
||||||
|
'@protobufjs/inquire': 1.1.0
|
||||||
|
'@protobufjs/path': 1.1.2
|
||||||
|
'@protobufjs/pool': 1.1.0
|
||||||
|
'@protobufjs/utf8': 1.1.0
|
||||||
|
'@types/node': 25.3.0
|
||||||
|
long: 5.3.2
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
ipaddr.js: 1.9.1
|
ipaddr.js: 1.9.1
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
pure-rand@6.1.0: {}
|
pure-rand@6.1.0: {}
|
||||||
@@ -3377,10 +3744,18 @@ snapshots:
|
|||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
destr: 2.0.5
|
destr: 2.0.5
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
real-require@0.2.0: {}
|
real-require@0.2.0: {}
|
||||||
|
|
||||||
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
@@ -3443,6 +3818,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safe-regex2@5.0.0:
|
safe-regex2@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ret: 0.5.0
|
ret: 0.5.0
|
||||||
@@ -3528,8 +3905,18 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
split-ca@1.0.1: {}
|
||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
|
ssh2@1.17.0:
|
||||||
|
dependencies:
|
||||||
|
asn1: 0.2.6
|
||||||
|
bcrypt-pbkdf: 1.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
cpu-features: 0.0.10
|
||||||
|
nan: 2.25.0
|
||||||
|
|
||||||
stackback@0.0.2: {}
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
@@ -3542,6 +3929,10 @@ snapshots:
|
|||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
@@ -3550,6 +3941,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
dependencies:
|
||||||
|
chownr: 1.1.4
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
pump: 3.0.3
|
||||||
|
tar-stream: 2.2.0
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
bl: 4.1.0
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
fs-constants: 1.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
thread-stream@4.0.0:
|
thread-stream@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
real-require: 0.2.0
|
real-require: 0.2.0
|
||||||
@@ -3582,6 +3988,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
tweetnacl@0.14.5: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
@@ -3594,6 +4002,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
undici-types@5.26.5: {}
|
||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
@@ -3602,6 +4012,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
uuid@10.0.0: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0):
|
vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0):
|
||||||
@@ -3672,8 +4086,28 @@ snapshots:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
dependencies:
|
||||||
|
cliui: 8.0.1
|
||||||
|
escalade: 3.2.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
string-width: 4.2.3
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
yoctocolors-cjs@2.1.3: {}
|
yoctocolors-cjs@2.1.3: {}
|
||||||
|
|||||||
105
src/cli/src/registry/client.ts
Normal file
105
src/cli/src/registry/client.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import type { RegistryServer, SearchOptions, RegistryClientConfig, RegistryName } from './types.js';
|
||||||
|
import { RegistrySource } from './base.js';
|
||||||
|
import { OfficialRegistrySource } from './sources/official.js';
|
||||||
|
import { GlamaRegistrySource } from './sources/glama.js';
|
||||||
|
import { SmitheryRegistrySource } from './sources/smithery.js';
|
||||||
|
import { RegistryCache } from './cache.js';
|
||||||
|
import { deduplicateResults } from './dedup.js';
|
||||||
|
import { rankResults } from './ranking.js';
|
||||||
|
|
||||||
|
export class RegistryClient {
|
||||||
|
private sources: Map<RegistryName, RegistrySource>;
|
||||||
|
private cache: RegistryCache;
|
||||||
|
private enabledRegistries: RegistryName[];
|
||||||
|
private metrics = {
|
||||||
|
queryLatencies: new Map<string, number[]>(),
|
||||||
|
errorCounts: new Map<string, number>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(config: RegistryClientConfig = {}) {
|
||||||
|
this.enabledRegistries = config.registries ?? ['official', 'glama', 'smithery'];
|
||||||
|
this.cache = new RegistryCache(config.cacheTTLMs);
|
||||||
|
|
||||||
|
this.sources = new Map<RegistryName, RegistrySource>([
|
||||||
|
['official', new OfficialRegistrySource()],
|
||||||
|
['glama', new GlamaRegistrySource()],
|
||||||
|
['smithery', new SmitheryRegistrySource()],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(options: SearchOptions): Promise<RegistryServer[]> {
|
||||||
|
// Check cache
|
||||||
|
const cached = this.cache.get(options.query, options);
|
||||||
|
if (cached !== null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registries = options.registries ?? this.enabledRegistries;
|
||||||
|
const limit = options.limit ?? 20;
|
||||||
|
|
||||||
|
// Query all enabled registries in parallel
|
||||||
|
const promises = registries
|
||||||
|
.map((name) => this.sources.get(name))
|
||||||
|
.filter((source): source is RegistrySource => source !== undefined)
|
||||||
|
.map(async (source) => {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const results = await source.search(options.query, limit);
|
||||||
|
this.recordLatency(source.name, Date.now() - start);
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
this.recordError(source.name);
|
||||||
|
// Graceful degradation: log and continue
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const settled = await Promise.all(promises);
|
||||||
|
let combined = settled.flat();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (options.verified === true) {
|
||||||
|
combined = combined.filter((s) => s.verified);
|
||||||
|
}
|
||||||
|
if (options.transport !== undefined) {
|
||||||
|
combined = combined.filter((s) => s.transport === options.transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate, rank, and limit
|
||||||
|
const deduped = deduplicateResults(combined);
|
||||||
|
const ranked = rankResults(deduped, options.query);
|
||||||
|
const results = ranked.slice(0, limit);
|
||||||
|
|
||||||
|
// Cache results
|
||||||
|
this.cache.set(options.query, options, results);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCacheMetrics(): { hits: number; misses: number; ratio: number } {
|
||||||
|
return this.cache.getHitRatio();
|
||||||
|
}
|
||||||
|
|
||||||
|
getQueryLatencies(): Map<string, number[]> {
|
||||||
|
return new Map(this.metrics.queryLatencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorCounts(): Map<string, number> {
|
||||||
|
return new Map(this.metrics.errorCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordLatency(source: string, ms: number): void {
|
||||||
|
const existing = this.metrics.queryLatencies.get(source) ?? [];
|
||||||
|
existing.push(ms);
|
||||||
|
this.metrics.queryLatencies.set(source, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordError(source: string): void {
|
||||||
|
const count = this.metrics.errorCounts.get(source) ?? 0;
|
||||||
|
this.metrics.errorCounts.set(source, count + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/cli/src/registry/index.ts
Normal file
17
src/cli/src/registry/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export { RegistryClient } from './client.js';
|
||||||
|
export { RegistryCache } from './cache.js';
|
||||||
|
export { RegistrySource } from './base.js';
|
||||||
|
export { deduplicateResults } from './dedup.js';
|
||||||
|
export { rankResults } from './ranking.js';
|
||||||
|
export { withRetry } from './retry.js';
|
||||||
|
export { OfficialRegistrySource } from './sources/official.js';
|
||||||
|
export { GlamaRegistrySource } from './sources/glama.js';
|
||||||
|
export { SmitheryRegistrySource } from './sources/smithery.js';
|
||||||
|
export type {
|
||||||
|
RegistryServer,
|
||||||
|
SearchOptions,
|
||||||
|
RegistryClientConfig,
|
||||||
|
RegistryName,
|
||||||
|
EnvVar,
|
||||||
|
} from './types.js';
|
||||||
|
export { sanitizeString } from './types.js';
|
||||||
63
src/cli/src/registry/ranking.ts
Normal file
63
src/cli/src/registry/ranking.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { RegistryServer } from './types.js';
|
||||||
|
|
||||||
|
const WEIGHT_RELEVANCE = 0.4;
|
||||||
|
const WEIGHT_POPULARITY = 0.3;
|
||||||
|
const WEIGHT_VERIFIED = 0.2;
|
||||||
|
const WEIGHT_RECENCY = 0.1;
|
||||||
|
|
||||||
|
function textRelevance(server: RegistryServer, query: string): number {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
const name = server.name.toLowerCase();
|
||||||
|
const desc = server.description.toLowerCase();
|
||||||
|
|
||||||
|
// Exact name match
|
||||||
|
if (name === q) return 1.0;
|
||||||
|
// Name starts with query
|
||||||
|
if (name.startsWith(q)) return 0.9;
|
||||||
|
// Name contains query
|
||||||
|
if (name.includes(q)) return 0.7;
|
||||||
|
// Description contains query
|
||||||
|
if (desc.includes(q)) return 0.4;
|
||||||
|
|
||||||
|
// Word-level matching
|
||||||
|
const queryWords = q.split(/\s+/);
|
||||||
|
const matchCount = queryWords.filter(
|
||||||
|
(w) => name.includes(w) || desc.includes(w),
|
||||||
|
).length;
|
||||||
|
return queryWords.length > 0 ? (matchCount / queryWords.length) * 0.3 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function popularityScore(server: RegistryServer): number {
|
||||||
|
// Normalize to 0-1 range; use log scale since popularity can vary hugely
|
||||||
|
if (server.popularityScore <= 0) return 0;
|
||||||
|
// Log scale: log10(1) = 0, log10(10000) ≈ 4 → normalize to 0-1 with cap at 100k
|
||||||
|
return Math.min(Math.log10(server.popularityScore + 1) / 5, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifiedScore(server: RegistryServer): number {
|
||||||
|
return server.verified ? 1.0 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recencyScore(server: RegistryServer): number {
|
||||||
|
if (server.lastUpdated === undefined) return 0.5; // Unknown = middle score
|
||||||
|
const ageMs = Date.now() - server.lastUpdated.getTime();
|
||||||
|
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
||||||
|
// Less than 30 days = 1.0, decays to 0 at 365 days
|
||||||
|
return Math.max(0, 1 - ageDays / 365);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeScore(server: RegistryServer, query: string): number {
|
||||||
|
return (
|
||||||
|
WEIGHT_RELEVANCE * textRelevance(server, query) +
|
||||||
|
WEIGHT_POPULARITY * popularityScore(server) +
|
||||||
|
WEIGHT_VERIFIED * verifiedScore(server) +
|
||||||
|
WEIGHT_RECENCY * recencyScore(server)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rankResults(
|
||||||
|
results: RegistryServer[],
|
||||||
|
query: string,
|
||||||
|
): RegistryServer[] {
|
||||||
|
return [...results].sort((a, b) => computeScore(b, query) - computeScore(a, query));
|
||||||
|
}
|
||||||
16
src/cli/src/registry/retry.ts
Normal file
16
src/cli/src/registry/retry.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export async function withRetry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxRetries = 3,
|
||||||
|
baseDelay = 1000,
|
||||||
|
): Promise<T> {
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === maxRetries - 1) throw error;
|
||||||
|
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
|
||||||
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
}
|
||||||
92
src/cli/src/registry/sources/glama.ts
Normal file
92
src/cli/src/registry/sources/glama.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { RegistrySource } from '../base.js';
|
||||||
|
import {
|
||||||
|
GlamaRegistryResponseSchema,
|
||||||
|
sanitizeString,
|
||||||
|
type GlamaServerEntry,
|
||||||
|
type RegistryServer,
|
||||||
|
} from '../types.js';
|
||||||
|
import { withRetry } from '../retry.js';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://glama.ai/api/mcp/v1/servers';
|
||||||
|
|
||||||
|
export class GlamaRegistrySource extends RegistrySource {
|
||||||
|
readonly name = 'glama' as const;
|
||||||
|
|
||||||
|
async search(query: string, limit: number): Promise<RegistryServer[]> {
|
||||||
|
const results: RegistryServer[] = [];
|
||||||
|
let cursor: string | null | undefined;
|
||||||
|
|
||||||
|
while (results.length < limit) {
|
||||||
|
const url = new URL(BASE_URL);
|
||||||
|
url.searchParams.set('query', query);
|
||||||
|
if (cursor !== undefined && cursor !== null) {
|
||||||
|
url.searchParams.set('after', cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await withRetry(() => fetch(url.toString()));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Glama registry returned ${String(response.status)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw: unknown = await response.json();
|
||||||
|
const parsed = GlamaRegistryResponseSchema.parse(raw);
|
||||||
|
|
||||||
|
for (const entry of parsed.servers) {
|
||||||
|
results.push(this.normalizeResult(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.pageInfo.hasNextPage || parsed.servers.length === 0) break;
|
||||||
|
cursor = parsed.pageInfo.endCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected normalizeResult(raw: unknown): RegistryServer {
|
||||||
|
const entry = raw as GlamaServerEntry;
|
||||||
|
|
||||||
|
// Extract env vars from JSON Schema
|
||||||
|
const props = entry.environmentVariablesJsonSchema?.properties ?? {};
|
||||||
|
const envTemplate = Object.entries(props).map(([name, schemaProp]) => {
|
||||||
|
const envVar: import('../types.js').EnvVar = {
|
||||||
|
name,
|
||||||
|
description: sanitizeString(schemaProp.description ?? ''),
|
||||||
|
isSecret: name.toLowerCase().includes('token') ||
|
||||||
|
name.toLowerCase().includes('secret') ||
|
||||||
|
name.toLowerCase().includes('password') ||
|
||||||
|
name.toLowerCase().includes('key'),
|
||||||
|
};
|
||||||
|
if (schemaProp.default !== undefined) {
|
||||||
|
envVar.defaultValue = schemaProp.default;
|
||||||
|
}
|
||||||
|
return envVar;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine transport from attributes
|
||||||
|
const attrs = entry.attributes;
|
||||||
|
let transport: RegistryServer['transport'] = 'stdio';
|
||||||
|
if (attrs.includes('hosting:remote-capable') || attrs.includes('hosting:hybrid')) {
|
||||||
|
transport = 'sse';
|
||||||
|
}
|
||||||
|
|
||||||
|
const packages: RegistryServer['packages'] = {};
|
||||||
|
if (entry.slug !== '') {
|
||||||
|
packages.npm = entry.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: RegistryServer = {
|
||||||
|
name: sanitizeString(entry.name),
|
||||||
|
description: sanitizeString(entry.description),
|
||||||
|
packages,
|
||||||
|
envTemplate,
|
||||||
|
transport,
|
||||||
|
popularityScore: 0, // Glama has no popularity metrics in list
|
||||||
|
verified: attrs.includes('author:official'),
|
||||||
|
sourceRegistry: 'glama',
|
||||||
|
};
|
||||||
|
if (entry.repository?.url !== undefined) {
|
||||||
|
result.repositoryUrl = entry.repository.url;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/cli/src/registry/sources/official.ts
Normal file
106
src/cli/src/registry/sources/official.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { RegistrySource } from '../base.js';
|
||||||
|
import {
|
||||||
|
OfficialRegistryResponseSchema,
|
||||||
|
sanitizeString,
|
||||||
|
type OfficialServerEntry,
|
||||||
|
type RegistryServer,
|
||||||
|
} from '../types.js';
|
||||||
|
import { withRetry } from '../retry.js';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://registry.modelcontextprotocol.io/v0/servers';
|
||||||
|
|
||||||
|
export class OfficialRegistrySource extends RegistrySource {
|
||||||
|
readonly name = 'official' as const;
|
||||||
|
|
||||||
|
async search(query: string, limit: number): Promise<RegistryServer[]> {
|
||||||
|
const results: RegistryServer[] = [];
|
||||||
|
let cursor: string | null | undefined;
|
||||||
|
|
||||||
|
while (results.length < limit) {
|
||||||
|
const url = new URL(BASE_URL);
|
||||||
|
url.searchParams.set('search', query);
|
||||||
|
url.searchParams.set('limit', String(Math.min(limit - results.length, 100)));
|
||||||
|
if (cursor !== undefined && cursor !== null) {
|
||||||
|
url.searchParams.set('cursor', cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await withRetry(() => fetch(url.toString()));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Official registry returned ${String(response.status)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw: unknown = await response.json();
|
||||||
|
const parsed = OfficialRegistryResponseSchema.parse(raw);
|
||||||
|
|
||||||
|
for (const entry of parsed.servers) {
|
||||||
|
results.push(this.normalizeResult(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = parsed.metadata?.nextCursor;
|
||||||
|
if (cursor === null || cursor === undefined || parsed.servers.length === 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected normalizeResult(raw: unknown): RegistryServer {
|
||||||
|
const entry = raw as OfficialServerEntry;
|
||||||
|
const server = entry.server;
|
||||||
|
|
||||||
|
// Extract env vars from packages
|
||||||
|
const envTemplate = server.packages.flatMap((pkg: { environmentVariables: Array<{ name: string; description?: string; isSecret?: boolean }> }) =>
|
||||||
|
pkg.environmentVariables.map((ev: { name: string; description?: string; isSecret?: boolean }) => ({
|
||||||
|
name: ev.name,
|
||||||
|
description: sanitizeString(ev.description ?? ''),
|
||||||
|
isSecret: ev.isSecret ?? false,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine transport from packages or remotes
|
||||||
|
let transport: RegistryServer['transport'] = 'stdio';
|
||||||
|
if (server.packages.length > 0) {
|
||||||
|
const pkgTransport = server.packages[0]?.transport?.type;
|
||||||
|
if (pkgTransport === 'stdio') transport = 'stdio';
|
||||||
|
}
|
||||||
|
if (server.remotes.length > 0) {
|
||||||
|
const remoteType = server.remotes[0]?.type;
|
||||||
|
if (remoteType === 'sse') transport = 'sse';
|
||||||
|
else if (remoteType === 'streamable-http') transport = 'streamable-http';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract npm package identifier
|
||||||
|
const npmPkg = server.packages.find((p: { registryType: string }) => p.registryType === 'npm');
|
||||||
|
const dockerPkg = server.packages.find((p: { registryType: string }) => p.registryType === 'oci');
|
||||||
|
|
||||||
|
// Extract dates from _meta
|
||||||
|
const meta = entry._meta as Record<string, Record<string, unknown>> | undefined;
|
||||||
|
const officialMeta = meta?.['io.modelcontextprotocol.registry/official'];
|
||||||
|
const updatedAt = officialMeta?.['updatedAt'];
|
||||||
|
|
||||||
|
const packages: RegistryServer['packages'] = {};
|
||||||
|
if (npmPkg !== undefined) {
|
||||||
|
packages.npm = npmPkg.identifier;
|
||||||
|
}
|
||||||
|
if (dockerPkg !== undefined) {
|
||||||
|
packages.docker = dockerPkg.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: RegistryServer = {
|
||||||
|
name: sanitizeString(server.title ?? server.name),
|
||||||
|
description: sanitizeString(server.description),
|
||||||
|
packages,
|
||||||
|
envTemplate,
|
||||||
|
transport,
|
||||||
|
popularityScore: 0, // Official registry has no popularity data
|
||||||
|
verified: false, // Official registry has no verified badges
|
||||||
|
sourceRegistry: 'official',
|
||||||
|
};
|
||||||
|
if (server.repository?.url !== undefined) {
|
||||||
|
result.repositoryUrl = server.repository.url;
|
||||||
|
}
|
||||||
|
if (typeof updatedAt === 'string') {
|
||||||
|
result.lastUpdated = new Date(updatedAt);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/cli/src/registry/sources/smithery.ts
Normal file
62
src/cli/src/registry/sources/smithery.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { RegistrySource } from '../base.js';
|
||||||
|
import {
|
||||||
|
SmitheryRegistryResponseSchema,
|
||||||
|
sanitizeString,
|
||||||
|
type SmitheryServerEntry,
|
||||||
|
type RegistryServer,
|
||||||
|
} from '../types.js';
|
||||||
|
import { withRetry } from '../retry.js';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://registry.smithery.ai/servers';
|
||||||
|
|
||||||
|
export class SmitheryRegistrySource extends RegistrySource {
|
||||||
|
readonly name = 'smithery' as const;
|
||||||
|
|
||||||
|
async search(query: string, limit: number): Promise<RegistryServer[]> {
|
||||||
|
const results: RegistryServer[] = [];
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
while (results.length < limit) {
|
||||||
|
const url = new URL(BASE_URL);
|
||||||
|
url.searchParams.set('q', query);
|
||||||
|
url.searchParams.set('pageSize', String(Math.min(limit - results.length, 50)));
|
||||||
|
url.searchParams.set('page', String(page));
|
||||||
|
|
||||||
|
const response = await withRetry(() => fetch(url.toString()));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Smithery registry returned ${String(response.status)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw: unknown = await response.json();
|
||||||
|
const parsed = SmitheryRegistryResponseSchema.parse(raw);
|
||||||
|
|
||||||
|
for (const entry of parsed.servers) {
|
||||||
|
results.push(this.normalizeResult(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page >= parsed.pagination.totalPages || parsed.servers.length === 0) break;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected normalizeResult(raw: unknown): RegistryServer {
|
||||||
|
const entry = raw as SmitheryServerEntry;
|
||||||
|
|
||||||
|
const result: RegistryServer = {
|
||||||
|
name: sanitizeString(entry.displayName !== '' ? entry.displayName : entry.qualifiedName),
|
||||||
|
description: sanitizeString(entry.description),
|
||||||
|
packages: {},
|
||||||
|
envTemplate: [], // Smithery doesn't include env vars in list view
|
||||||
|
transport: entry.remote ? 'sse' : 'stdio',
|
||||||
|
popularityScore: entry.useCount,
|
||||||
|
verified: entry.verified,
|
||||||
|
sourceRegistry: 'smithery',
|
||||||
|
};
|
||||||
|
if (entry.createdAt !== undefined) {
|
||||||
|
result.lastUpdated = new Date(entry.createdAt);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,7 +173,7 @@ export type SmitheryServerEntry = z.infer<typeof SmitheryServerSchema>;
|
|||||||
|
|
||||||
// ── Security utilities ──
|
// ── Security utilities ──
|
||||||
|
|
||||||
const ANSI_ESCAPE_RE = /\x1b\[[0-9;]*[a-zA-Z]|[\x00-\x08\x0B\x0C\x0E-\x1F]/g;
|
const ANSI_ESCAPE_RE = /\x1b\[[0-9;]*[a-zA-Z]|[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F]|\x1b/g;
|
||||||
|
|
||||||
export function sanitizeString(text: string): string {
|
export function sanitizeString(text: string): string {
|
||||||
return text.replace(ANSI_ESCAPE_RE, '');
|
return text.replace(ANSI_ESCAPE_RE, '');
|
||||||
|
|||||||
90
src/cli/tests/registry/cache.test.ts
Normal file
90
src/cli/tests/registry/cache.test.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { RegistryCache } from '../../src/registry/cache.js';
|
||||||
|
import type { RegistryServer, SearchOptions } from '../../src/registry/types.js';
|
||||||
|
|
||||||
|
function makeServer(name: string): RegistryServer {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description: `${name} server`,
|
||||||
|
packages: {},
|
||||||
|
envTemplate: [],
|
||||||
|
transport: 'stdio',
|
||||||
|
popularityScore: 0,
|
||||||
|
verified: false,
|
||||||
|
sourceRegistry: 'official',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: SearchOptions = { query: 'test' };
|
||||||
|
|
||||||
|
describe('RegistryCache', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for cache miss', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
expect(cache.get('unknown', defaultOptions)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns data for cache hit within TTL', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
const data = [makeServer('test')];
|
||||||
|
cache.set('test', defaultOptions, data);
|
||||||
|
expect(cache.get('test', defaultOptions)).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null after TTL expires', () => {
|
||||||
|
const cache = new RegistryCache(1000); // 1 second TTL
|
||||||
|
cache.set('test', defaultOptions, [makeServer('test')]);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1001);
|
||||||
|
expect(cache.get('test', defaultOptions)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates deterministic cache keys', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
const data = [makeServer('test')];
|
||||||
|
cache.set('query', { query: 'query', limit: 10 }, data);
|
||||||
|
expect(cache.get('query', { query: 'query', limit: 10 })).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates different keys for different queries', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
cache.set('a', { query: 'a' }, [makeServer('a')]);
|
||||||
|
expect(cache.get('b', { query: 'b' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks hits and misses correctly', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
cache.set('test', defaultOptions, [makeServer('test')]);
|
||||||
|
|
||||||
|
cache.get('test', defaultOptions); // hit
|
||||||
|
cache.get('test', defaultOptions); // hit
|
||||||
|
cache.get('miss', { query: 'miss' }); // miss
|
||||||
|
|
||||||
|
const ratio = cache.getHitRatio();
|
||||||
|
expect(ratio.hits).toBe(2);
|
||||||
|
expect(ratio.misses).toBe(1);
|
||||||
|
expect(ratio.ratio).toBeCloseTo(2 / 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 ratio when no accesses', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
expect(cache.getHitRatio().ratio).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears all entries and resets metrics', () => {
|
||||||
|
const cache = new RegistryCache();
|
||||||
|
cache.set('a', { query: 'a' }, [makeServer('a')]);
|
||||||
|
cache.get('a', { query: 'a' }); // hit
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
expect(cache.get('a', { query: 'a' })).toBeNull();
|
||||||
|
expect(cache.size).toBe(0);
|
||||||
|
expect(cache.getHitRatio().hits).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
282
src/cli/tests/registry/client.test.ts
Normal file
282
src/cli/tests/registry/client.test.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { RegistryClient } from '../../src/registry/client.js';
|
||||||
|
import type { RegistryServer } from '../../src/registry/types.js';
|
||||||
|
|
||||||
|
function makeServer(name: string, source: 'official' | 'glama' | 'smithery'): RegistryServer {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description: `${name} description`,
|
||||||
|
packages: { npm: `@test/${name}` },
|
||||||
|
envTemplate: [],
|
||||||
|
transport: 'stdio',
|
||||||
|
popularityScore: 50,
|
||||||
|
verified: source === 'smithery',
|
||||||
|
sourceRegistry: source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock fetch globally
|
||||||
|
const mockFetch = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('fetch', mockFetch);
|
||||||
|
mockFetch.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockRegistryResponse(source: string, servers: RegistryServer[]): void {
|
||||||
|
mockFetch.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('registry.modelcontextprotocol.io')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: servers
|
||||||
|
.filter((s) => s.sourceRegistry === 'official')
|
||||||
|
.map((s) => ({
|
||||||
|
server: {
|
||||||
|
name: s.name,
|
||||||
|
description: s.description,
|
||||||
|
packages: s.packages.npm !== undefined ? [{
|
||||||
|
registryType: 'npm',
|
||||||
|
identifier: s.packages.npm,
|
||||||
|
transport: { type: 'stdio' },
|
||||||
|
environmentVariables: [],
|
||||||
|
}] : [],
|
||||||
|
remotes: [],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
metadata: { nextCursor: null, count: 1 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url.includes('glama.ai')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: servers
|
||||||
|
.filter((s) => s.sourceRegistry === 'glama')
|
||||||
|
.map((s) => ({
|
||||||
|
id: s.name,
|
||||||
|
name: s.name,
|
||||||
|
description: s.description,
|
||||||
|
attributes: [],
|
||||||
|
slug: s.packages.npm ?? '',
|
||||||
|
})),
|
||||||
|
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url.includes('registry.smithery.ai')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: servers
|
||||||
|
.filter((s) => s.sourceRegistry === 'smithery')
|
||||||
|
.map((s) => ({
|
||||||
|
qualifiedName: s.name,
|
||||||
|
displayName: s.name,
|
||||||
|
description: s.description,
|
||||||
|
verified: s.verified,
|
||||||
|
useCount: s.popularityScore,
|
||||||
|
remote: false,
|
||||||
|
})),
|
||||||
|
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 1 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`Unexpected URL: ${url}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RegistryClient', () => {
|
||||||
|
it('queries all enabled registries', async () => {
|
||||||
|
const testServers = [
|
||||||
|
makeServer('slack-official', 'official'),
|
||||||
|
makeServer('slack-glama', 'glama'),
|
||||||
|
makeServer('slack-smithery', 'smithery'),
|
||||||
|
];
|
||||||
|
mockRegistryResponse('all', testServers);
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
const results = await client.search({ query: 'slack' });
|
||||||
|
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses cached results on second call', async () => {
|
||||||
|
mockRegistryResponse('all', [makeServer('slack', 'official')]);
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
await client.search({ query: 'slack' });
|
||||||
|
mockFetch.mockClear();
|
||||||
|
await client.search({ query: 'slack' });
|
||||||
|
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by registry when specified', async () => {
|
||||||
|
mockRegistryResponse('all', [makeServer('test', 'official')]);
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
await client.search({ query: 'test', registries: ['official'] });
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||||
|
const calledUrl = mockFetch.mock.calls[0]?.[0] as string;
|
||||||
|
expect(calledUrl).toContain('modelcontextprotocol.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles partial failures gracefully', async () => {
|
||||||
|
mockFetch.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('glama.ai')) {
|
||||||
|
return Promise.reject(new Error('Network error'));
|
||||||
|
}
|
||||||
|
if (url.includes('registry.smithery.ai')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: [{
|
||||||
|
qualifiedName: 'slack',
|
||||||
|
displayName: 'Slack',
|
||||||
|
description: 'Slack',
|
||||||
|
verified: true,
|
||||||
|
useCount: 100,
|
||||||
|
remote: false,
|
||||||
|
}],
|
||||||
|
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 1 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: [],
|
||||||
|
metadata: { nextCursor: null },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
const results = await client.search({ query: 'slack' });
|
||||||
|
|
||||||
|
// Should still return results from successful sources
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records error counts on failures', async () => {
|
||||||
|
mockFetch.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('glama.ai')) {
|
||||||
|
return Promise.reject(new Error('fail'));
|
||||||
|
}
|
||||||
|
// Return empty for others
|
||||||
|
if (url.includes('modelcontextprotocol')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null } }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: [],
|
||||||
|
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 0 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
await client.search({ query: 'test' });
|
||||||
|
|
||||||
|
const errors = client.getErrorCounts();
|
||||||
|
expect(errors.get('glama')).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by verified when specified', async () => {
|
||||||
|
mockFetch.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('registry.smithery.ai')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: [
|
||||||
|
{ qualifiedName: 'verified', displayName: 'Verified', description: '', verified: true, useCount: 100, remote: false },
|
||||||
|
{ qualifiedName: 'unverified', displayName: 'Unverified', description: '', verified: false, useCount: 50, remote: false },
|
||||||
|
],
|
||||||
|
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 2 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null } }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock glama too
|
||||||
|
mockFetch.mockImplementation((url: string) => {
|
||||||
|
if (url.includes('registry.smithery.ai')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
servers: [
|
||||||
|
{ qualifiedName: 'verified', displayName: 'Verified', description: '', verified: true, useCount: 100, remote: false },
|
||||||
|
{ qualifiedName: 'unverified', displayName: 'Unverified', description: '', verified: false, useCount: 50, remote: false },
|
||||||
|
],
|
||||||
|
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 2 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url.includes('glama.ai')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ servers: [], pageInfo: { hasNextPage: false, hasPreviousPage: false } }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null } }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
const results = await client.search({ query: 'test', verified: true });
|
||||||
|
|
||||||
|
for (const r of results) {
|
||||||
|
expect(r.verified).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects limit option', async () => {
|
||||||
|
mockRegistryResponse('all', [
|
||||||
|
makeServer('a', 'official'),
|
||||||
|
makeServer('b', 'glama'),
|
||||||
|
makeServer('c', 'smithery'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
const results = await client.search({ query: 'test', limit: 1 });
|
||||||
|
expect(results.length).toBeLessThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records latency metrics', async () => {
|
||||||
|
mockRegistryResponse('all', [makeServer('test', 'official')]);
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
await client.search({ query: 'test' });
|
||||||
|
|
||||||
|
const latencies = client.getQueryLatencies();
|
||||||
|
expect(latencies.size).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearCache empties cache', async () => {
|
||||||
|
mockRegistryResponse('all', [makeServer('test', 'official')]);
|
||||||
|
|
||||||
|
const client = new RegistryClient();
|
||||||
|
await client.search({ query: 'test' });
|
||||||
|
client.clearCache();
|
||||||
|
mockFetch.mockClear();
|
||||||
|
mockRegistryResponse('all', [makeServer('test', 'official')]);
|
||||||
|
await client.search({ query: 'test' });
|
||||||
|
|
||||||
|
// Should have fetched again after cache clear
|
||||||
|
expect(mockFetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
105
src/cli/tests/registry/dedup.test.ts
Normal file
105
src/cli/tests/registry/dedup.test.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { deduplicateResults } from '../../src/registry/dedup.js';
|
||||||
|
import type { RegistryServer } from '../../src/registry/types.js';
|
||||||
|
|
||||||
|
function makeServer(overrides: Partial<RegistryServer> = {}): RegistryServer {
|
||||||
|
return {
|
||||||
|
name: 'test-server',
|
||||||
|
description: 'A test server',
|
||||||
|
packages: {},
|
||||||
|
envTemplate: [],
|
||||||
|
transport: 'stdio',
|
||||||
|
popularityScore: 0,
|
||||||
|
verified: false,
|
||||||
|
sourceRegistry: 'official',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('deduplicateResults', () => {
|
||||||
|
it('keeps unique servers', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'server-a', packages: { npm: 'pkg-a' } }),
|
||||||
|
makeServer({ name: 'server-b', packages: { npm: 'pkg-b' } }),
|
||||||
|
];
|
||||||
|
expect(deduplicateResults(servers)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates by npm package name, keeps higher popularity', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'low', packages: { npm: '@test/slack' }, popularityScore: 10, sourceRegistry: 'official' }),
|
||||||
|
makeServer({ name: 'high', packages: { npm: '@test/slack' }, popularityScore: 100, sourceRegistry: 'smithery' }),
|
||||||
|
];
|
||||||
|
const result = deduplicateResults(servers);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]?.name).toBe('high');
|
||||||
|
expect(result[0]?.popularityScore).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates by GitHub URL with different formats', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'a', repositoryUrl: 'https://github.com/org/repo', popularityScore: 5 }),
|
||||||
|
makeServer({ name: 'b', repositoryUrl: 'git@github.com:org/repo.git', popularityScore: 50 }),
|
||||||
|
];
|
||||||
|
const result = deduplicateResults(servers);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]?.name).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges envTemplate from both sources', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({
|
||||||
|
name: 'a',
|
||||||
|
packages: { npm: 'pkg' },
|
||||||
|
envTemplate: [{ name: 'TOKEN', description: 'API token', isSecret: true }],
|
||||||
|
popularityScore: 10,
|
||||||
|
}),
|
||||||
|
makeServer({
|
||||||
|
name: 'b',
|
||||||
|
packages: { npm: 'pkg' },
|
||||||
|
envTemplate: [{ name: 'URL', description: 'Base URL', isSecret: false }],
|
||||||
|
popularityScore: 5,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const result = deduplicateResults(servers);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]?.envTemplate).toHaveLength(2);
|
||||||
|
expect(result[0]?.envTemplate.map((e) => e.name)).toContain('TOKEN');
|
||||||
|
expect(result[0]?.envTemplate.map((e) => e.name)).toContain('URL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates envTemplate by var name', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({
|
||||||
|
packages: { npm: 'pkg' },
|
||||||
|
envTemplate: [{ name: 'TOKEN', description: 'from a', isSecret: true }],
|
||||||
|
popularityScore: 10,
|
||||||
|
}),
|
||||||
|
makeServer({
|
||||||
|
packages: { npm: 'pkg' },
|
||||||
|
envTemplate: [{ name: 'TOKEN', description: 'from b', isSecret: true }],
|
||||||
|
popularityScore: 5,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const result = deduplicateResults(servers);
|
||||||
|
expect(result[0]?.envTemplate).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges verified status (OR)', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ packages: { npm: 'pkg' }, verified: true, popularityScore: 10 }),
|
||||||
|
makeServer({ packages: { npm: 'pkg' }, verified: false, popularityScore: 5 }),
|
||||||
|
];
|
||||||
|
const result = deduplicateResults(servers);
|
||||||
|
expect(result[0]?.verified).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles servers with no npm or repo', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'a' }),
|
||||||
|
makeServer({ name: 'b' }),
|
||||||
|
];
|
||||||
|
// No matching key → no dedup
|
||||||
|
expect(deduplicateResults(servers)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
91
src/cli/tests/registry/ranking.test.ts
Normal file
91
src/cli/tests/registry/ranking.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { rankResults } from '../../src/registry/ranking.js';
|
||||||
|
import type { RegistryServer } from '../../src/registry/types.js';
|
||||||
|
|
||||||
|
function makeServer(overrides: Partial<RegistryServer> = {}): RegistryServer {
|
||||||
|
return {
|
||||||
|
name: 'test-server',
|
||||||
|
description: 'A test server',
|
||||||
|
packages: {},
|
||||||
|
envTemplate: [],
|
||||||
|
transport: 'stdio',
|
||||||
|
popularityScore: 0,
|
||||||
|
verified: false,
|
||||||
|
sourceRegistry: 'official',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('rankResults', () => {
|
||||||
|
it('puts exact name match first', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'slack-extended-tools' }),
|
||||||
|
makeServer({ name: 'slack' }),
|
||||||
|
makeServer({ name: 'my-slack-bot' }),
|
||||||
|
];
|
||||||
|
const ranked = rankResults(servers, 'slack');
|
||||||
|
expect(ranked[0]?.name).toBe('slack');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ranks verified servers higher than unverified', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'server-a', verified: false }),
|
||||||
|
makeServer({ name: 'server-b', verified: true }),
|
||||||
|
];
|
||||||
|
const ranked = rankResults(servers, 'server');
|
||||||
|
expect(ranked[0]?.name).toBe('server-b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ranks popular servers higher', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'unpopular', popularityScore: 1 }),
|
||||||
|
makeServer({ name: 'popular', popularityScore: 10000 }),
|
||||||
|
];
|
||||||
|
const ranked = rankResults(servers, 'test');
|
||||||
|
expect(ranked[0]?.name).toBe('popular');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('considers recency', () => {
|
||||||
|
const recent = new Date();
|
||||||
|
const old = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'old-server', lastUpdated: old }),
|
||||||
|
makeServer({ name: 'new-server', lastUpdated: recent }),
|
||||||
|
];
|
||||||
|
const ranked = rankResults(servers, 'test');
|
||||||
|
expect(ranked[0]?.name).toBe('new-server');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing lastUpdated gracefully', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'no-date' }),
|
||||||
|
makeServer({ name: 'has-date', lastUpdated: new Date() }),
|
||||||
|
];
|
||||||
|
// Should not throw
|
||||||
|
const ranked = rankResults(servers, 'test');
|
||||||
|
expect(ranked).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces stable ordering for identical scores', () => {
|
||||||
|
const servers = Array.from({ length: 10 }, (_, i) =>
|
||||||
|
makeServer({ name: `server-${String(i)}` }),
|
||||||
|
);
|
||||||
|
const ranked1 = rankResults(servers, 'test');
|
||||||
|
const ranked2 = rankResults(servers, 'test');
|
||||||
|
expect(ranked1.map((s) => s.name)).toEqual(ranked2.map((s) => s.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array for empty input', () => {
|
||||||
|
expect(rankResults([], 'test')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate original array', () => {
|
||||||
|
const servers = [
|
||||||
|
makeServer({ name: 'b' }),
|
||||||
|
makeServer({ name: 'a' }),
|
||||||
|
];
|
||||||
|
const original = [...servers];
|
||||||
|
rankResults(servers, 'test');
|
||||||
|
expect(servers.map((s) => s.name)).toEqual(original.map((s) => s.name));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
"@mcpctl/db": "workspace:*",
|
"@mcpctl/db": "workspace:*",
|
||||||
"@mcpctl/shared": "workspace:*",
|
"@mcpctl/shared": "workspace:*",
|
||||||
"@prisma/client": "^6.0.0",
|
"@prisma/client": "^6.0.0",
|
||||||
|
"dockerode": "^4.0.9",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"zod": "^3.24.0"
|
"zod": "^3.24.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dockerode": "^4.0.1",
|
||||||
"@types/node": "^25.3.0"
|
"@types/node": "^25.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type { IMcpServerRepository, IMcpProfileRepository } from './interfaces.js';
|
export type { IMcpServerRepository, IMcpProfileRepository, IMcpInstanceRepository } from './interfaces.js';
|
||||||
export { McpServerRepository } from './mcp-server.repository.js';
|
export { McpServerRepository } from './mcp-server.repository.js';
|
||||||
export { McpProfileRepository } from './mcp-profile.repository.js';
|
export { McpProfileRepository } from './mcp-profile.repository.js';
|
||||||
export type { IProjectRepository } from './project.repository.js';
|
export type { IProjectRepository } from './project.repository.js';
|
||||||
export { ProjectRepository } from './project.repository.js';
|
export { ProjectRepository } from './project.repository.js';
|
||||||
|
export { McpInstanceRepository } from './mcp-instance.repository.js';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { McpServer, McpProfile } from '@prisma/client';
|
import type { McpServer, McpProfile, McpInstance, InstanceStatus } from '@prisma/client';
|
||||||
import type { CreateMcpServerInput, UpdateMcpServerInput } from '../validation/mcp-server.schema.js';
|
import type { CreateMcpServerInput, UpdateMcpServerInput } from '../validation/mcp-server.schema.js';
|
||||||
import type { CreateMcpProfileInput, UpdateMcpProfileInput } from '../validation/mcp-profile.schema.js';
|
import type { CreateMcpProfileInput, UpdateMcpProfileInput } from '../validation/mcp-profile.schema.js';
|
||||||
|
|
||||||
@@ -11,6 +11,15 @@ export interface IMcpServerRepository {
|
|||||||
delete(id: string): Promise<void>;
|
delete(id: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMcpInstanceRepository {
|
||||||
|
findAll(serverId?: string): Promise<McpInstance[]>;
|
||||||
|
findById(id: string): Promise<McpInstance | null>;
|
||||||
|
findByContainerId(containerId: string): Promise<McpInstance | null>;
|
||||||
|
create(data: { serverId: string; containerId?: string; status?: InstanceStatus; port?: number; metadata?: Record<string, unknown> }): Promise<McpInstance>;
|
||||||
|
updateStatus(id: string, status: InstanceStatus, fields?: { containerId?: string; port?: number; metadata?: Record<string, unknown> }): Promise<McpInstance>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IMcpProfileRepository {
|
export interface IMcpProfileRepository {
|
||||||
findAll(serverId?: string): Promise<McpProfile[]>;
|
findAll(serverId?: string): Promise<McpProfile[]>;
|
||||||
findById(id: string): Promise<McpProfile | null>;
|
findById(id: string): Promise<McpProfile | null>;
|
||||||
|
|||||||
71
src/mcpd/src/repositories/mcp-instance.repository.ts
Normal file
71
src/mcpd/src/repositories/mcp-instance.repository.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { PrismaClient, McpInstance, InstanceStatus, Prisma } from '@prisma/client';
|
||||||
|
import type { IMcpInstanceRepository } from './interfaces.js';
|
||||||
|
|
||||||
|
export class McpInstanceRepository implements IMcpInstanceRepository {
|
||||||
|
constructor(private prisma: PrismaClient) {}
|
||||||
|
|
||||||
|
async findAll(serverId?: string): Promise<McpInstance[]> {
|
||||||
|
const where: Prisma.McpInstanceWhereInput = {};
|
||||||
|
if (serverId) {
|
||||||
|
where.serverId = serverId;
|
||||||
|
}
|
||||||
|
return this.prisma.mcpInstance.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<McpInstance | null> {
|
||||||
|
return this.prisma.mcpInstance.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByContainerId(containerId: string): Promise<McpInstance | null> {
|
||||||
|
return this.prisma.mcpInstance.findFirst({ where: { containerId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: {
|
||||||
|
serverId: string;
|
||||||
|
containerId?: string;
|
||||||
|
status?: InstanceStatus;
|
||||||
|
port?: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}): Promise<McpInstance> {
|
||||||
|
return this.prisma.mcpInstance.create({
|
||||||
|
data: {
|
||||||
|
serverId: data.serverId,
|
||||||
|
containerId: data.containerId ?? null,
|
||||||
|
status: data.status ?? 'STOPPED',
|
||||||
|
port: data.port ?? null,
|
||||||
|
metadata: (data.metadata ?? {}) as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStatus(
|
||||||
|
id: string,
|
||||||
|
status: InstanceStatus,
|
||||||
|
fields?: { containerId?: string; port?: number; metadata?: Record<string, unknown> },
|
||||||
|
): Promise<McpInstance> {
|
||||||
|
const updateData: Prisma.McpInstanceUpdateInput = {
|
||||||
|
status,
|
||||||
|
version: { increment: 1 },
|
||||||
|
};
|
||||||
|
if (fields?.containerId !== undefined) {
|
||||||
|
updateData.containerId = fields.containerId;
|
||||||
|
}
|
||||||
|
if (fields?.port !== undefined) {
|
||||||
|
updateData.port = fields.port;
|
||||||
|
}
|
||||||
|
if (fields?.metadata !== undefined) {
|
||||||
|
updateData.metadata = fields.metadata as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
return this.prisma.mcpInstance.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.prisma.mcpInstance.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export type { HealthDeps } from './health.js';
|
|||||||
export { registerMcpServerRoutes } from './mcp-servers.js';
|
export { registerMcpServerRoutes } from './mcp-servers.js';
|
||||||
export { registerMcpProfileRoutes } from './mcp-profiles.js';
|
export { registerMcpProfileRoutes } from './mcp-profiles.js';
|
||||||
export { registerProjectRoutes } from './projects.js';
|
export { registerProjectRoutes } from './projects.js';
|
||||||
|
export { registerInstanceRoutes } from './instances.js';
|
||||||
|
|||||||
49
src/mcpd/src/routes/instances.ts
Normal file
49
src/mcpd/src/routes/instances.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
import type { InstanceService } from '../services/instance.service.js';
|
||||||
|
|
||||||
|
export function registerInstanceRoutes(app: FastifyInstance, service: InstanceService): void {
|
||||||
|
app.get<{ Querystring: { serverId?: string } }>('/api/v1/instances', async (request) => {
|
||||||
|
return service.list(request.query.serverId);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get<{ Params: { id: string } }>('/api/v1/instances/:id', async (request) => {
|
||||||
|
return service.getById(request.params.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post<{ Body: { serverId: string; env?: Record<string, string>; hostPort?: number } }>(
|
||||||
|
'/api/v1/instances',
|
||||||
|
async (request, reply) => {
|
||||||
|
const { serverId } = request.body;
|
||||||
|
const opts: { env?: Record<string, string>; hostPort?: number } = {};
|
||||||
|
if (request.body.env) {
|
||||||
|
opts.env = request.body.env;
|
||||||
|
}
|
||||||
|
if (request.body.hostPort !== undefined) {
|
||||||
|
opts.hostPort = request.body.hostPort;
|
||||||
|
}
|
||||||
|
const instance = await service.start(serverId, opts);
|
||||||
|
reply.code(201);
|
||||||
|
return instance;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post<{ Params: { id: string } }>('/api/v1/instances/:id/stop', async (request) => {
|
||||||
|
return service.stop(request.params.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete<{ Params: { id: string } }>('/api/v1/instances/:id', async (request, reply) => {
|
||||||
|
await service.remove(request.params.id);
|
||||||
|
reply.code(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get<{ Params: { id: string }; Querystring: { tail?: string } }>(
|
||||||
|
'/api/v1/instances/:id/logs',
|
||||||
|
async (request) => {
|
||||||
|
const opts: { tail?: number } = {};
|
||||||
|
if (request.query.tail) {
|
||||||
|
opts.tail = parseInt(request.query.tail, 10);
|
||||||
|
}
|
||||||
|
return service.getLogs(request.params.id, opts);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
156
src/mcpd/src/services/docker/container-manager.ts
Normal file
156
src/mcpd/src/services/docker/container-manager.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import Docker from 'dockerode';
|
||||||
|
import type {
|
||||||
|
McpOrchestrator,
|
||||||
|
ContainerSpec,
|
||||||
|
ContainerInfo,
|
||||||
|
ContainerLogs,
|
||||||
|
} from '../orchestrator.js';
|
||||||
|
import { DEFAULT_MEMORY_LIMIT, DEFAULT_NANO_CPUS } from '../orchestrator.js';
|
||||||
|
|
||||||
|
const MCPCTL_LABEL = 'mcpctl.managed';
|
||||||
|
|
||||||
|
function mapState(state: string | undefined): ContainerInfo['state'] {
|
||||||
|
switch (state?.toLowerCase()) {
|
||||||
|
case 'running':
|
||||||
|
return 'running';
|
||||||
|
case 'created':
|
||||||
|
case 'restarting':
|
||||||
|
return 'starting';
|
||||||
|
case 'exited':
|
||||||
|
case 'dead':
|
||||||
|
case 'paused':
|
||||||
|
return 'stopped';
|
||||||
|
default:
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockerContainerManager implements McpOrchestrator {
|
||||||
|
private docker: Docker;
|
||||||
|
|
||||||
|
constructor(opts?: Docker.DockerOptions) {
|
||||||
|
this.docker = new Docker(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ping(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.docker.ping();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pullImage(image: string): Promise<void> {
|
||||||
|
const stream = await this.docker.pull(image);
|
||||||
|
// Wait for pull to complete
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
this.docker.modem.followProgress(stream, (err: Error | null) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createContainer(spec: ContainerSpec): Promise<ContainerInfo> {
|
||||||
|
const memoryLimit = spec.memoryLimit ?? DEFAULT_MEMORY_LIMIT;
|
||||||
|
const nanoCpus = spec.nanoCpus ?? DEFAULT_NANO_CPUS;
|
||||||
|
|
||||||
|
const portBindings: Record<string, Array<{ HostPort: string }>> = {};
|
||||||
|
const exposedPorts: Record<string, Record<string, never>> = {};
|
||||||
|
|
||||||
|
if (spec.containerPort) {
|
||||||
|
const key = `${spec.containerPort}/tcp`;
|
||||||
|
exposedPorts[key] = {};
|
||||||
|
portBindings[key] = [{ HostPort: spec.hostPort ? String(spec.hostPort) : '0' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
[MCPCTL_LABEL]: 'true',
|
||||||
|
...spec.labels,
|
||||||
|
};
|
||||||
|
|
||||||
|
const envArr = spec.env
|
||||||
|
? Object.entries(spec.env).map(([k, v]) => `${k}=${v}`)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const container = await this.docker.createContainer({
|
||||||
|
Image: spec.image,
|
||||||
|
name: spec.name,
|
||||||
|
Env: envArr,
|
||||||
|
ExposedPorts: exposedPorts,
|
||||||
|
Labels: labels,
|
||||||
|
HostConfig: {
|
||||||
|
PortBindings: portBindings,
|
||||||
|
Memory: memoryLimit,
|
||||||
|
NanoCpus: nanoCpus,
|
||||||
|
NetworkMode: spec.network ?? 'bridge',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
return this.inspectContainer(container.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopContainer(containerId: string, timeoutSeconds = 10): Promise<void> {
|
||||||
|
const container = this.docker.getContainer(containerId);
|
||||||
|
await container.stop({ t: timeoutSeconds });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeContainer(containerId: string, force = false): Promise<void> {
|
||||||
|
const container = this.docker.getContainer(containerId);
|
||||||
|
await container.remove({ force, v: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async inspectContainer(containerId: string): Promise<ContainerInfo> {
|
||||||
|
const container = this.docker.getContainer(containerId);
|
||||||
|
const info = await container.inspect();
|
||||||
|
|
||||||
|
let port: number | undefined;
|
||||||
|
const ports = info.NetworkSettings?.Ports;
|
||||||
|
if (ports) {
|
||||||
|
for (const bindings of Object.values(ports)) {
|
||||||
|
const arr = bindings as Array<{ HostIp: string; HostPort: string }> | undefined;
|
||||||
|
if (arr && arr.length > 0 && arr[0]) {
|
||||||
|
port = parseInt(arr[0].HostPort, 10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ContainerInfo = {
|
||||||
|
containerId: info.Id,
|
||||||
|
name: info.Name.replace(/^\//, ''),
|
||||||
|
state: mapState(info.State?.Status),
|
||||||
|
createdAt: new Date(info.Created),
|
||||||
|
};
|
||||||
|
if (port !== undefined) {
|
||||||
|
result.port = port;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContainerLogs(
|
||||||
|
containerId: string,
|
||||||
|
opts?: { tail?: number; since?: number },
|
||||||
|
): Promise<ContainerLogs> {
|
||||||
|
const container = this.docker.getContainer(containerId);
|
||||||
|
const logOpts: Record<string, unknown> = {
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
follow: false,
|
||||||
|
tail: opts?.tail ?? 100,
|
||||||
|
};
|
||||||
|
if (opts?.since) {
|
||||||
|
logOpts['since'] = opts.since;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await container.logs(logOpts) as unknown as Buffer;
|
||||||
|
const raw = buffer.toString('utf-8');
|
||||||
|
|
||||||
|
// Docker multiplexes stdout/stderr with 8-byte headers.
|
||||||
|
// For simplicity we return everything as stdout.
|
||||||
|
return { stdout: raw, stderr: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
export { McpServerService, NotFoundError, ConflictError } from './mcp-server.service.js';
|
export { McpServerService, NotFoundError, ConflictError } from './mcp-server.service.js';
|
||||||
export { McpProfileService } from './mcp-profile.service.js';
|
export { McpProfileService } from './mcp-profile.service.js';
|
||||||
export { ProjectService } from './project.service.js';
|
export { ProjectService } from './project.service.js';
|
||||||
|
export { InstanceService } from './instance.service.js';
|
||||||
export { generateMcpConfig } from './mcp-config-generator.js';
|
export { generateMcpConfig } from './mcp-config-generator.js';
|
||||||
export type { McpConfig, McpConfigServer, ProfileWithServer } from './mcp-config-generator.js';
|
export type { McpConfig, McpConfigServer, ProfileWithServer } from './mcp-config-generator.js';
|
||||||
|
export type { McpOrchestrator, ContainerSpec, ContainerInfo, ContainerLogs } from './orchestrator.js';
|
||||||
|
export { DEFAULT_MEMORY_LIMIT, DEFAULT_NANO_CPUS } from './orchestrator.js';
|
||||||
|
export { DockerContainerManager } from './docker/container-manager.js';
|
||||||
|
|||||||
112
src/mcpd/src/services/instance.service.ts
Normal file
112
src/mcpd/src/services/instance.service.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import type { McpInstance } from '@prisma/client';
|
||||||
|
import type { IMcpInstanceRepository, IMcpServerRepository } from '../repositories/interfaces.js';
|
||||||
|
import type { McpOrchestrator, ContainerSpec } from './orchestrator.js';
|
||||||
|
import { NotFoundError } from './mcp-server.service.js';
|
||||||
|
|
||||||
|
export class InstanceService {
|
||||||
|
constructor(
|
||||||
|
private instanceRepo: IMcpInstanceRepository,
|
||||||
|
private serverRepo: IMcpServerRepository,
|
||||||
|
private orchestrator: McpOrchestrator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async list(serverId?: string): Promise<McpInstance[]> {
|
||||||
|
return this.instanceRepo.findAll(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: string): Promise<McpInstance> {
|
||||||
|
const instance = await this.instanceRepo.findById(id);
|
||||||
|
if (!instance) throw new NotFoundError(`Instance '${id}' not found`);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(serverId: string, opts?: { env?: Record<string, string>; hostPort?: number }): Promise<McpInstance> {
|
||||||
|
const server = await this.serverRepo.findById(serverId);
|
||||||
|
if (!server) throw new NotFoundError(`McpServer '${serverId}' not found`);
|
||||||
|
|
||||||
|
const image = server.dockerImage ?? server.packageName ?? server.name;
|
||||||
|
|
||||||
|
// Create DB record first in STARTING state
|
||||||
|
let instance = await this.instanceRepo.create({
|
||||||
|
serverId,
|
||||||
|
status: 'STARTING',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const spec: ContainerSpec = {
|
||||||
|
image,
|
||||||
|
name: `mcpctl-${server.name}-${instance.id}`,
|
||||||
|
hostPort: opts?.hostPort ?? null,
|
||||||
|
labels: {
|
||||||
|
'mcpctl.server-id': serverId,
|
||||||
|
'mcpctl.instance-id': instance.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (server.transport === 'SSE' || server.transport === 'STREAMABLE_HTTP') {
|
||||||
|
spec.containerPort = 3000;
|
||||||
|
}
|
||||||
|
if (opts?.env) {
|
||||||
|
spec.env = opts.env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerInfo = await this.orchestrator.createContainer(spec);
|
||||||
|
|
||||||
|
const updateFields: { containerId: string; port?: number } = {
|
||||||
|
containerId: containerInfo.containerId,
|
||||||
|
};
|
||||||
|
if (containerInfo.port !== undefined) {
|
||||||
|
updateFields.port = containerInfo.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = await this.instanceRepo.updateStatus(instance.id, 'RUNNING', updateFields);
|
||||||
|
} catch (err) {
|
||||||
|
// Mark as ERROR if container creation fails
|
||||||
|
instance = await this.instanceRepo.updateStatus(instance.id, 'ERROR', {
|
||||||
|
metadata: { error: err instanceof Error ? err.message : String(err) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(id: string): Promise<McpInstance> {
|
||||||
|
const instance = await this.getById(id);
|
||||||
|
if (!instance.containerId) {
|
||||||
|
return this.instanceRepo.updateStatus(id, 'STOPPED');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.instanceRepo.updateStatus(id, 'STOPPING');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.orchestrator.stopContainer(instance.containerId);
|
||||||
|
return await this.instanceRepo.updateStatus(id, 'STOPPED');
|
||||||
|
} catch (err) {
|
||||||
|
return await this.instanceRepo.updateStatus(id, 'ERROR', {
|
||||||
|
metadata: { error: err instanceof Error ? err.message : String(err) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const instance = await this.getById(id);
|
||||||
|
|
||||||
|
if (instance.containerId) {
|
||||||
|
try {
|
||||||
|
await this.orchestrator.removeContainer(instance.containerId, true);
|
||||||
|
} catch {
|
||||||
|
// Container may already be gone, proceed with DB cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.instanceRepo.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLogs(id: string, opts?: { tail?: number }): Promise<{ stdout: string; stderr: string }> {
|
||||||
|
const instance = await this.getById(id);
|
||||||
|
if (!instance.containerId) {
|
||||||
|
return { stdout: '', stderr: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orchestrator.getContainerLogs(instance.containerId, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/mcpd/src/services/orchestrator.ts
Normal file
64
src/mcpd/src/services/orchestrator.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Container orchestrator abstraction. Implementations can back onto Docker, Podman, or Kubernetes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ContainerSpec {
|
||||||
|
/** Docker/OCI image reference */
|
||||||
|
image: string;
|
||||||
|
/** Human-readable name (used as container name prefix) */
|
||||||
|
name: string;
|
||||||
|
/** Environment variables */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
/** Host port to bind (null = auto-assign) */
|
||||||
|
hostPort?: number | null;
|
||||||
|
/** Container port to expose */
|
||||||
|
containerPort?: number;
|
||||||
|
/** Memory limit in bytes (default: 512 MB) */
|
||||||
|
memoryLimit?: number;
|
||||||
|
/** CPU period quota (nanoCPUs, default: 0.5 CPU) */
|
||||||
|
nanoCpus?: number;
|
||||||
|
/** Labels for identification / filtering */
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
/** Network name to attach to */
|
||||||
|
network?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerInfo {
|
||||||
|
containerId: string;
|
||||||
|
name: string;
|
||||||
|
state: 'running' | 'stopped' | 'starting' | 'error' | 'unknown';
|
||||||
|
port?: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerLogs {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpOrchestrator {
|
||||||
|
/** Pull an image if not present locally */
|
||||||
|
pullImage(image: string): Promise<void>;
|
||||||
|
|
||||||
|
/** Create and start a container */
|
||||||
|
createContainer(spec: ContainerSpec): Promise<ContainerInfo>;
|
||||||
|
|
||||||
|
/** Stop a running container */
|
||||||
|
stopContainer(containerId: string, timeoutSeconds?: number): Promise<void>;
|
||||||
|
|
||||||
|
/** Remove a stopped container */
|
||||||
|
removeContainer(containerId: string, force?: boolean): Promise<void>;
|
||||||
|
|
||||||
|
/** Get container info */
|
||||||
|
inspectContainer(containerId: string): Promise<ContainerInfo>;
|
||||||
|
|
||||||
|
/** Get container logs */
|
||||||
|
getContainerLogs(containerId: string, opts?: { tail?: number; since?: number }): Promise<ContainerLogs>;
|
||||||
|
|
||||||
|
/** Check if the orchestrator runtime is available */
|
||||||
|
ping(): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default resource limits */
|
||||||
|
export const DEFAULT_MEMORY_LIMIT = 512 * 1024 * 1024; // 512 MB
|
||||||
|
export const DEFAULT_NANO_CPUS = 500_000_000; // 0.5 CPU
|
||||||
125
src/mcpd/tests/container-manager.test.ts
Normal file
125
src/mcpd/tests/container-manager.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { DockerContainerManager } from '../src/services/docker/container-manager.js';
|
||||||
|
import type { ContainerSpec } from '../src/services/orchestrator.js';
|
||||||
|
|
||||||
|
// Mock dockerode
|
||||||
|
vi.mock('dockerode', () => {
|
||||||
|
const mockContainer = {
|
||||||
|
id: 'ctr-abc123',
|
||||||
|
start: vi.fn(async () => {}),
|
||||||
|
stop: vi.fn(async () => {}),
|
||||||
|
remove: vi.fn(async () => {}),
|
||||||
|
inspect: vi.fn(async () => ({
|
||||||
|
Id: 'ctr-abc123',
|
||||||
|
Name: '/mcpctl-test',
|
||||||
|
State: { Status: 'running' },
|
||||||
|
Created: '2024-01-01T00:00:00Z',
|
||||||
|
NetworkSettings: {
|
||||||
|
Ports: {
|
||||||
|
'3000/tcp': [{ HostIp: '0.0.0.0', HostPort: '32768' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
logs: vi.fn(async () => Buffer.from('test log output')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockModem = {
|
||||||
|
followProgress: vi.fn((_stream: unknown, onFinished: (err: Error | null) => void) => {
|
||||||
|
onFinished(null);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockDocker {
|
||||||
|
modem = mockModem;
|
||||||
|
ping = vi.fn(async () => 'OK');
|
||||||
|
pull = vi.fn(async () => ({}));
|
||||||
|
createContainer = vi.fn(async () => mockContainer);
|
||||||
|
getContainer = vi.fn(() => mockContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { default: MockDocker };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DockerContainerManager', () => {
|
||||||
|
let manager: DockerContainerManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
manager = new DockerContainerManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ping', () => {
|
||||||
|
it('returns true when Docker is available', async () => {
|
||||||
|
expect(await manager.ping()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pullImage', () => {
|
||||||
|
it('pulls an image', async () => {
|
||||||
|
await manager.pullImage('node:20-alpine');
|
||||||
|
// No error = success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createContainer', () => {
|
||||||
|
it('creates and starts a container', async () => {
|
||||||
|
const spec: ContainerSpec = {
|
||||||
|
image: 'ghcr.io/slack-mcp:latest',
|
||||||
|
name: 'mcpctl-slack-inst1',
|
||||||
|
env: { SLACK_TOKEN: 'xoxb-test' },
|
||||||
|
containerPort: 3000,
|
||||||
|
hostPort: null,
|
||||||
|
labels: { 'mcpctl.server-id': 'srv-1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await manager.createContainer(spec);
|
||||||
|
|
||||||
|
expect(result.containerId).toBe('ctr-abc123');
|
||||||
|
expect(result.name).toBe('mcpctl-test');
|
||||||
|
expect(result.state).toBe('running');
|
||||||
|
expect(result.port).toBe(32768);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies resource limits', async () => {
|
||||||
|
const spec: ContainerSpec = {
|
||||||
|
image: 'test:latest',
|
||||||
|
name: 'test-container',
|
||||||
|
memoryLimit: 256 * 1024 * 1024,
|
||||||
|
nanoCpus: 250_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
await manager.createContainer(spec);
|
||||||
|
// The mock captures the call - we verify it doesn't throw
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopContainer', () => {
|
||||||
|
it('stops a container', async () => {
|
||||||
|
await manager.stopContainer('ctr-abc123');
|
||||||
|
// No error = success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeContainer', () => {
|
||||||
|
it('removes a container', async () => {
|
||||||
|
await manager.removeContainer('ctr-abc123', true);
|
||||||
|
// No error = success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inspectContainer', () => {
|
||||||
|
it('returns container info with mapped state', async () => {
|
||||||
|
const info = await manager.inspectContainer('ctr-abc123');
|
||||||
|
expect(info.containerId).toBe('ctr-abc123');
|
||||||
|
expect(info.state).toBe('running');
|
||||||
|
expect(info.port).toBe(32768);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getContainerLogs', () => {
|
||||||
|
it('returns container logs', async () => {
|
||||||
|
const logs = await manager.getContainerLogs('ctr-abc123', { tail: 50 });
|
||||||
|
expect(logs.stdout).toBe('test log output');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
253
src/mcpd/tests/instance-service.test.ts
Normal file
253
src/mcpd/tests/instance-service.test.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { InstanceService } from '../src/services/instance.service.js';
|
||||||
|
import { NotFoundError } from '../src/services/mcp-server.service.js';
|
||||||
|
import type { IMcpInstanceRepository, IMcpServerRepository } from '../src/repositories/interfaces.js';
|
||||||
|
import type { McpOrchestrator } from '../src/services/orchestrator.js';
|
||||||
|
|
||||||
|
function mockInstanceRepo(): IMcpInstanceRepository {
|
||||||
|
return {
|
||||||
|
findAll: vi.fn(async () => []),
|
||||||
|
findById: vi.fn(async () => null),
|
||||||
|
findByContainerId: vi.fn(async () => null),
|
||||||
|
create: vi.fn(async (data) => ({
|
||||||
|
id: 'inst-1',
|
||||||
|
serverId: data.serverId,
|
||||||
|
containerId: data.containerId ?? null,
|
||||||
|
status: data.status ?? 'STOPPED',
|
||||||
|
port: data.port ?? null,
|
||||||
|
metadata: data.metadata ?? {},
|
||||||
|
version: 1,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})),
|
||||||
|
updateStatus: vi.fn(async (id, status, fields) => ({
|
||||||
|
id,
|
||||||
|
serverId: 'srv-1',
|
||||||
|
containerId: fields?.containerId ?? 'ctr-abc',
|
||||||
|
status,
|
||||||
|
port: fields?.port ?? null,
|
||||||
|
metadata: fields?.metadata ?? {},
|
||||||
|
version: 2,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})),
|
||||||
|
delete: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockServerRepo(): IMcpServerRepository {
|
||||||
|
return {
|
||||||
|
findAll: vi.fn(async () => []),
|
||||||
|
findById: vi.fn(async () => null),
|
||||||
|
findByName: vi.fn(async () => null),
|
||||||
|
create: vi.fn(async () => ({} as never)),
|
||||||
|
update: vi.fn(async () => ({} as never)),
|
||||||
|
delete: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockOrchestrator(): McpOrchestrator {
|
||||||
|
return {
|
||||||
|
ping: vi.fn(async () => true),
|
||||||
|
pullImage: vi.fn(async () => {}),
|
||||||
|
createContainer: vi.fn(async (spec) => ({
|
||||||
|
containerId: 'ctr-abc123',
|
||||||
|
name: spec.name,
|
||||||
|
state: 'running' as const,
|
||||||
|
port: 3000,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})),
|
||||||
|
stopContainer: vi.fn(async () => {}),
|
||||||
|
removeContainer: vi.fn(async () => {}),
|
||||||
|
inspectContainer: vi.fn(async () => ({
|
||||||
|
containerId: 'ctr-abc123',
|
||||||
|
name: 'test',
|
||||||
|
state: 'running' as const,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})),
|
||||||
|
getContainerLogs: vi.fn(async () => ({ stdout: 'log output', stderr: '' })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('InstanceService', () => {
|
||||||
|
let instanceRepo: ReturnType<typeof mockInstanceRepo>;
|
||||||
|
let serverRepo: ReturnType<typeof mockServerRepo>;
|
||||||
|
let orchestrator: ReturnType<typeof mockOrchestrator>;
|
||||||
|
let service: InstanceService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
instanceRepo = mockInstanceRepo();
|
||||||
|
serverRepo = mockServerRepo();
|
||||||
|
orchestrator = mockOrchestrator();
|
||||||
|
service = new InstanceService(instanceRepo, serverRepo, orchestrator);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('lists all instances', async () => {
|
||||||
|
const result = await service.list();
|
||||||
|
expect(instanceRepo.findAll).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by serverId', async () => {
|
||||||
|
await service.list('srv-1');
|
||||||
|
expect(instanceRepo.findAll).toHaveBeenCalledWith('srv-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getById', () => {
|
||||||
|
it('throws NotFoundError when not found', async () => {
|
||||||
|
await expect(service.getById('missing')).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns instance when found', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({ id: 'inst-1' } as never);
|
||||||
|
const result = await service.getById('inst-1');
|
||||||
|
expect(result.id).toBe('inst-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('start', () => {
|
||||||
|
it('throws NotFoundError for unknown server', async () => {
|
||||||
|
await expect(service.start('missing')).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates instance and starts container', async () => {
|
||||||
|
vi.mocked(serverRepo.findById).mockResolvedValue({
|
||||||
|
id: 'srv-1', name: 'slack', dockerImage: 'ghcr.io/slack-mcp:latest',
|
||||||
|
packageName: null, transport: 'STDIO', description: '', repositoryUrl: null,
|
||||||
|
envTemplate: [], version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.start('srv-1');
|
||||||
|
|
||||||
|
expect(instanceRepo.create).toHaveBeenCalledWith({
|
||||||
|
serverId: 'srv-1',
|
||||||
|
status: 'STARTING',
|
||||||
|
});
|
||||||
|
expect(orchestrator.createContainer).toHaveBeenCalled();
|
||||||
|
expect(instanceRepo.updateStatus).toHaveBeenCalledWith(
|
||||||
|
'inst-1', 'RUNNING',
|
||||||
|
expect.objectContaining({ containerId: 'ctr-abc123' }),
|
||||||
|
);
|
||||||
|
expect(result.status).toBe('RUNNING');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks instance as ERROR on container failure', async () => {
|
||||||
|
vi.mocked(serverRepo.findById).mockResolvedValue({
|
||||||
|
id: 'srv-1', name: 'slack', dockerImage: 'ghcr.io/slack-mcp:latest',
|
||||||
|
packageName: null, transport: 'STDIO', description: '', repositoryUrl: null,
|
||||||
|
envTemplate: [], version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
vi.mocked(orchestrator.createContainer).mockRejectedValue(new Error('Docker unavailable'));
|
||||||
|
|
||||||
|
const result = await service.start('srv-1');
|
||||||
|
|
||||||
|
expect(instanceRepo.updateStatus).toHaveBeenCalledWith(
|
||||||
|
'inst-1', 'ERROR',
|
||||||
|
expect.objectContaining({ metadata: { error: 'Docker unavailable' } }),
|
||||||
|
);
|
||||||
|
expect(result.status).toBe('ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses dockerImage for container spec', async () => {
|
||||||
|
vi.mocked(serverRepo.findById).mockResolvedValue({
|
||||||
|
id: 'srv-1', name: 'slack', dockerImage: 'myregistry.com/slack:v1',
|
||||||
|
packageName: '@slack/mcp', transport: 'SSE', description: '', repositoryUrl: null,
|
||||||
|
envTemplate: [], version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.start('srv-1');
|
||||||
|
|
||||||
|
const spec = vi.mocked(orchestrator.createContainer).mock.calls[0]?.[0];
|
||||||
|
expect(spec?.image).toBe('myregistry.com/slack:v1');
|
||||||
|
expect(spec?.containerPort).toBe(3000); // SSE transport
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stop', () => {
|
||||||
|
it('throws NotFoundError for missing instance', async () => {
|
||||||
|
await expect(service.stop('missing')).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops a running container', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||||
|
id: 'inst-1', containerId: 'ctr-abc', status: 'RUNNING',
|
||||||
|
serverId: 'srv-1', port: 3000, metadata: {},
|
||||||
|
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.stop('inst-1');
|
||||||
|
|
||||||
|
expect(orchestrator.stopContainer).toHaveBeenCalledWith('ctr-abc');
|
||||||
|
expect(instanceRepo.updateStatus).toHaveBeenCalledWith('inst-1', 'STOPPED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles stop without containerId', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||||
|
id: 'inst-1', containerId: null, status: 'ERROR',
|
||||||
|
serverId: 'srv-1', port: null, metadata: {},
|
||||||
|
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.stop('inst-1');
|
||||||
|
|
||||||
|
expect(orchestrator.stopContainer).not.toHaveBeenCalled();
|
||||||
|
expect(instanceRepo.updateStatus).toHaveBeenCalledWith('inst-1', 'STOPPED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
it('removes container and DB record', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||||
|
id: 'inst-1', containerId: 'ctr-abc', status: 'STOPPED',
|
||||||
|
serverId: 'srv-1', port: null, metadata: {},
|
||||||
|
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.remove('inst-1');
|
||||||
|
|
||||||
|
expect(orchestrator.removeContainer).toHaveBeenCalledWith('ctr-abc', true);
|
||||||
|
expect(instanceRepo.delete).toHaveBeenCalledWith('inst-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes DB record even if container is already gone', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||||
|
id: 'inst-1', containerId: 'ctr-abc', status: 'STOPPED',
|
||||||
|
serverId: 'srv-1', port: null, metadata: {},
|
||||||
|
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
vi.mocked(orchestrator.removeContainer).mockRejectedValue(new Error('No such container'));
|
||||||
|
|
||||||
|
await service.remove('inst-1');
|
||||||
|
|
||||||
|
expect(instanceRepo.delete).toHaveBeenCalledWith('inst-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogs', () => {
|
||||||
|
it('returns empty logs for instance without container', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||||
|
id: 'inst-1', containerId: null, status: 'ERROR',
|
||||||
|
serverId: 'srv-1', port: null, metadata: {},
|
||||||
|
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getLogs('inst-1');
|
||||||
|
expect(result).toEqual({ stdout: '', stderr: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns container logs', async () => {
|
||||||
|
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||||
|
id: 'inst-1', containerId: 'ctr-abc', status: 'RUNNING',
|
||||||
|
serverId: 'srv-1', port: 3000, metadata: {},
|
||||||
|
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getLogs('inst-1', { tail: 50 });
|
||||||
|
|
||||||
|
expect(orchestrator.getContainerLogs).toHaveBeenCalledWith('ctr-abc', { tail: 50 });
|
||||||
|
expect(result.stdout).toBe('log output');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user