feat: add Docker container management for MCP servers
McpOrchestrator interface with DockerContainerManager implementation, instance service for lifecycle management, instance API routes, and docker-compose with mcpd service. 127 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,35 @@ services:
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
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:
|
||||
image: postgres:16-alpine
|
||||
@@ -32,6 +61,15 @@ services:
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
profiles:
|
||||
- test
|
||||
|
||||
networks:
|
||||
mcpctl:
|
||||
driver: bridge
|
||||
mcp-servers:
|
||||
driver: bridge
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
mcpctl-pgdata:
|
||||
|
||||
431
pnpm-lock.yaml
generated
431
pnpm-lock.yaml
generated
@@ -112,6 +112,9 @@ importers:
|
||||
'@prisma/client':
|
||||
specifier: ^6.0.0
|
||||
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:
|
||||
specifier: ^5.0.0
|
||||
version: 5.7.4
|
||||
@@ -119,6 +122,9 @@ importers:
|
||||
specifier: ^3.24.0
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/dockerode':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
'@types/node':
|
||||
specifier: ^25.3.0
|
||||
version: 25.3.0
|
||||
@@ -148,6 +154,9 @@ packages:
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@balena/dockerignore@1.0.2':
|
||||
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -365,6 +374,20 @@ packages:
|
||||
'@fastify/rate-limit@10.3.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
@@ -531,6 +554,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
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':
|
||||
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -578,6 +604,36 @@ packages:
|
||||
'@prisma/get-platform@6.19.2':
|
||||
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':
|
||||
resolution: {integrity: sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==}
|
||||
cpu: [arm]
|
||||
@@ -712,6 +768,12 @@ packages:
|
||||
'@types/deep-eql@4.0.2':
|
||||
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':
|
||||
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||
|
||||
@@ -724,9 +786,15 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@18.19.130':
|
||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||
|
||||
'@types/node@25.3.0':
|
||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||
|
||||
'@types/ssh2@1.15.5':
|
||||
resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.0':
|
||||
resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -866,6 +934,9 @@ packages:
|
||||
argparse@2.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -887,6 +958,15 @@ packages:
|
||||
resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
|
||||
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:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -898,6 +978,13 @@ packages:
|
||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -933,6 +1020,9 @@ packages:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
citty@0.1.6:
|
||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||
|
||||
@@ -943,6 +1033,10 @@ packages:
|
||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -985,6 +1079,10 @@ packages:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1019,6 +1117,14 @@ packages:
|
||||
destr@2.0.5:
|
||||
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:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1044,6 +1150,9 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1064,6 +1173,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
@@ -1225,6 +1338,9 @@ packages:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1233,6 +1349,10 @@ packages:
|
||||
function-bind@1.1.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1291,6 +1411,9 @@ packages:
|
||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -1403,6 +1526,12 @@ packages:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -1449,6 +1578,9 @@ packages:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
mnemonist@0.40.0:
|
||||
resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==}
|
||||
|
||||
@@ -1459,6 +1591,9 @@ packages:
|
||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
nan@2.25.0:
|
||||
resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -1595,10 +1730,17 @@ packages:
|
||||
process-warning@5.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
pump@3.0.3:
|
||||
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1624,6 +1766,10 @@ packages:
|
||||
rc9@2.1.2:
|
||||
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
@@ -1632,6 +1778,10 @@ packages:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1671,6 +1821,9 @@ packages:
|
||||
rxjs@7.8.2:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safe-regex2@5.0.0:
|
||||
resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
|
||||
|
||||
@@ -1741,10 +1894,17 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
split-ca@1.0.1:
|
||||
resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
ssh2@1.17.0:
|
||||
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
@@ -1759,6 +1919,9 @@ packages:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1767,6 +1930,13 @@ packages:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -1808,6 +1978,9 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tweetnacl@0.14.5:
|
||||
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1821,6 +1994,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
@@ -1831,6 +2007,13 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1927,9 +2110,25 @@ packages:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1961,6 +2160,8 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@balena/dockerignore@1.0.2': {}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
@@ -2110,6 +2311,25 @@ snapshots:
|
||||
fastify-plugin: 5.1.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)':
|
||||
dependencies:
|
||||
hono: 4.12.0
|
||||
@@ -2259,6 +2479,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@js-sdsl/ordered-map@4.4.2': {}
|
||||
|
||||
'@lukeed/ms@2.0.2': {}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)':
|
||||
@@ -2320,6 +2542,29 @@ snapshots:
|
||||
dependencies:
|
||||
'@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':
|
||||
optional: true
|
||||
|
||||
@@ -2404,6 +2649,17 @@ snapshots:
|
||||
|
||||
'@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/estree@1.0.8': {}
|
||||
@@ -2412,10 +2668,18 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@18.19.130':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@25.3.0':
|
||||
dependencies:
|
||||
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)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -2599,6 +2863,10 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
asn1@0.2.6:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-v8-to-istanbul@0.3.11:
|
||||
@@ -2618,6 +2886,18 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -2640,6 +2920,14 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
c12@3.1.0:
|
||||
@@ -2677,6 +2965,8 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
citty@0.1.6:
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
@@ -2685,6 +2975,12 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@@ -2712,6 +3008,12 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
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:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -2734,6 +3036,27 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
@@ -2755,6 +3078,10 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
@@ -2794,6 +3121,8 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
@@ -3014,11 +3343,15 @@ snapshots:
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -3088,6 +3421,8 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
@@ -3182,6 +3517,10 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
lru-cache@11.2.6: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
@@ -3220,6 +3559,8 @@ snapshots:
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mnemonist@0.40.0:
|
||||
dependencies:
|
||||
obliterator: 2.0.5
|
||||
@@ -3228,6 +3569,9 @@ snapshots:
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
nan@2.25.0:
|
||||
optional: true
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -3351,11 +3695,31 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
pump@3.0.3:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
pure-rand@6.1.0: {}
|
||||
@@ -3380,10 +3744,18 @@ snapshots:
|
||||
defu: 6.1.4
|
||||
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: {}
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -3446,6 +3818,8 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-regex2@5.0.0:
|
||||
dependencies:
|
||||
ret: 0.5.0
|
||||
@@ -3531,8 +3905,18 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
split-ca@1.0.1: {}
|
||||
|
||||
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: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
@@ -3545,6 +3929,10 @@ snapshots:
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
@@ -3553,6 +3941,21 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
@@ -3585,6 +3988,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tweetnacl@0.14.5: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -3597,6 +4002,8 @@ snapshots:
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
@@ -3605,6 +4012,10 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0):
|
||||
@@ -3675,8 +4086,28 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
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: {}
|
||||
|
||||
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: {}
|
||||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
"@mcpctl/db": "workspace:*",
|
||||
"@mcpctl/shared": "workspace:*",
|
||||
"@prisma/client": "^6.0.0",
|
||||
"dockerode": "^4.0.9",
|
||||
"fastify": "^5.0.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "^4.0.1",
|
||||
"@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 { McpProfileRepository } from './mcp-profile.repository.js';
|
||||
export type { IProjectRepository } 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 { CreateMcpProfileInput, UpdateMcpProfileInput } from '../validation/mcp-profile.schema.js';
|
||||
|
||||
@@ -11,6 +11,15 @@ export interface IMcpServerRepository {
|
||||
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 {
|
||||
findAll(serverId?: string): Promise<McpProfile[]>;
|
||||
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 { registerMcpProfileRoutes } from './mcp-profiles.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 { McpProfileService } from './mcp-profile.service.js';
|
||||
export { ProjectService } from './project.service.js';
|
||||
export { InstanceService } from './instance.service.js';
|
||||
export { generateMcpConfig } 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