From b0d846671ad9b493f565b78a1ee48af0d9f7bf67 Mon Sep 17 00:00:00 2001 From: carson Date: Fri, 5 Sep 2025 09:34:08 -0700 Subject: [PATCH 1/5] Add MCP tool for starting and stoping various sei chain versions using docker - Added environment variable requirement to turn it on --- packages/mcp-server/.env.example | 5 + packages/mcp-server/package.json | 2 + .../mcp-server/src/core/services/chain.ts | 287 ++++++++ packages/mcp-server/src/docker/index.ts | 2 + packages/mcp-server/src/docker/initialize.ts | 86 +++ packages/mcp-server/src/docker/releases.ts | 41 ++ packages/mcp-server/src/docker/tools.ts | 678 ++++++++++++++++++ packages/mcp-server/src/server/server.ts | 2 + pnpm-lock.yaml | 196 +++++ 9 files changed, 1299 insertions(+) create mode 100644 packages/mcp-server/src/core/services/chain.ts create mode 100644 packages/mcp-server/src/docker/index.ts create mode 100644 packages/mcp-server/src/docker/initialize.ts create mode 100644 packages/mcp-server/src/docker/releases.ts create mode 100644 packages/mcp-server/src/docker/tools.ts diff --git a/packages/mcp-server/.env.example b/packages/mcp-server/.env.example index a768e2d50..6da6147c1 100644 --- a/packages/mcp-server/.env.example +++ b/packages/mcp-server/.env.example @@ -17,6 +17,11 @@ SERVER_PORT=8080 SERVER_HOST=localhost SERVER_PATH=/mcp +# Docker Configuration +# Enable Docker tools for local Sei chain development +# Set to 'true' to enable Docker container management tools +SEI_DOCKER_ENABLED=false + # RPC URLs # Optional, only needed if you want to use a custom RPC URL MAINNET_RPC_URL=your_mainnet_rpc_url_here diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index d64427d9f..f8a0b0882 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@jest/globals": "^30.0.3", "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "typescript": "^5.8.3" }, @@ -32,6 +33,7 @@ "@noble/hashes": "^1.8.0", "commander": "^14.0.0", "cors": "^2.8.5", + "dockerode": "^4.0.2", "dotenv": "^16.5.0", "express": "^4.21.2", "jest": "^30.0.3", diff --git a/packages/mcp-server/src/core/services/chain.ts b/packages/mcp-server/src/core/services/chain.ts new file mode 100644 index 000000000..60e9609a9 --- /dev/null +++ b/packages/mcp-server/src/core/services/chain.ts @@ -0,0 +1,287 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +/** + * Interface for Sei chain version information + */ +export interface SeiChainVersion { + tag: string; + digest: string; + created: string; + size: string; +} + +/** + * Interface for Docker container information + */ +export interface DockerContainer { + id: string; + name: string; + image: string; + status: string; + ports: string; + created: string; +} + +/** + * Get available Sei chain versions from GitHub Container Registry + * Uses skopeo to query the registry without pulling images + */ +export async function getAvailableSeiVersions(): Promise { + try { + // First try using skopeo (more reliable for registry queries) + try { + const { stdout } = await execAsync('skopeo list-tags docker://ghcr.io/sei-protocol/sei'); + const data = JSON.parse(stdout); + + // Convert tags to version objects with additional metadata + const versions: SeiChainVersion[] = []; + + for (const tag of data.Tags || []) { + try { + // Get image details for each tag + const { stdout: inspectOutput } = await execAsync(`skopeo inspect docker://ghcr.io/sei-protocol/sei:${tag}`); + const imageInfo = JSON.parse(inspectOutput); + + versions.push({ + tag, + digest: imageInfo.Digest || 'unknown', + created: imageInfo.Created || 'unknown', + size: imageInfo.Size ? `${Math.round(imageInfo.Size / 1024 / 1024)}MB` : 'unknown' + }); + } catch (error) { + // If we can't get details for a specific tag, still include it + versions.push({ + tag, + digest: 'unknown', + created: 'unknown', + size: 'unknown' + }); + } + } + + return versions.sort((a, b) => b.tag.localeCompare(a.tag, undefined, { numeric: true })); + } catch (skopeoError) { + // Fallback to docker registry API + console.warn('Skopeo not available, falling back to registry API'); + return await getVersionsFromRegistryAPI(); + } + } catch (error) { + throw new Error(`Failed to get available Sei versions: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Fallback method using GitHub Container Registry API + */ +async function getVersionsFromRegistryAPI(): Promise { + try { + // Use curl to query the GitHub Container Registry API + const { stdout } = await execAsync( + 'curl -s "https://ghcr.io/v2/sei-protocol/sei/tags/list" -H "Accept: application/vnd.docker.distribution.manifest.v2+json"' + ); + + const data = JSON.parse(stdout); + + if (!data.tags) { + throw new Error('No tags found in registry response'); + } + + // Convert to version objects (without detailed metadata in fallback) + const versions: SeiChainVersion[] = data.tags.map((tag: string) => ({ + tag, + digest: 'unknown', + created: 'unknown', + size: 'unknown' + })); + + return versions.sort((a, b) => b.tag.localeCompare(a.tag, undefined, { numeric: true })); + } catch (error) { + throw new Error(`Failed to query registry API: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Check if a specific Sei version exists in the registry + */ +export async function checkSeiVersionExists(version: string): Promise { + try { + const versions = await getAvailableSeiVersions(); + return versions.some((v) => v.tag === version || v.tag === `v${version}` || v.tag === version.replace('v', '')); + } catch (error) { + console.error('Error checking version existence:', error); + return false; + } +} + +/** + * Get the latest Sei version + */ +export async function getLatestSeiVersion(): Promise { + try { + const versions = await getAvailableSeiVersions(); + + // Filter out non-semantic versions and find the latest + const semanticVersions = versions.filter((v) => /^v?\d+\.\d+\.\d+$/.test(v.tag)); + + if (semanticVersions.length === 0) { + // Fallback to first available version + return versions[0]?.tag || 'latest'; + } + + return semanticVersions[0].tag; + } catch (error) { + throw new Error(`Failed to get latest Sei version: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Start a Sei chain Docker container + */ +export async function startSeiChain( + version = 'latest', + options: { + containerName?: string; + rpcPort?: number; + restPort?: number; + grpcPort?: number; + evmRpcPort?: number; + p2pPort?: number; + dataDir?: string; + chainId?: string; + moniker?: string; + } = {} +): Promise<{ containerId: string; containerName: string; ports: Record }> { + const { + containerName = `sei-chain-${version.replace(/[^a-zA-Z0-9]/g, '-')}`, + rpcPort = 26657, + restPort = 1317, + grpcPort = 9090, + evmRpcPort = 8545, + p2pPort = 26656, + dataDir = `./sei-data-${version}`, + chainId = 'sei-local', + moniker = 'sei-local-node' + } = options; + + try { + // Check if version exists + if (version !== 'latest') { + const exists = await checkSeiVersionExists(version); + if (!exists) { + throw new Error(`Version ${version} not found in registry`); + } + } + + // Stop existing container if it exists + try { + await execAsync(`docker stop ${containerName} 2>/dev/null || true`); + await execAsync(`docker rm ${containerName} 2>/dev/null || true`); + } catch (error) { + // Ignore errors when stopping/removing non-existent containers + } + + // Pull the image + const imageTag = `ghcr.io/sei-protocol/sei:${version}`; + + await execAsync(`docker pull ${imageTag}`); + + // Create data directory + await execAsync(`mkdir -p ${dataDir}`); + + // Build docker run command + const dockerCmd = [ + 'docker run -d', + `--name ${containerName}`, + `-p ${rpcPort}:26657`, + `-p ${restPort}:1317`, + `-p ${grpcPort}:9090`, + `-p ${evmRpcPort}:8545`, + `-p ${p2pPort}:26656`, + `-v ${dataDir}:/root/.sei`, + imageTag, + 'seid start', + `--chain-id ${chainId}`, + `--moniker ${moniker}`, + '--rpc.laddr tcp://0.0.0.0:26657', + '--api.enable true', + '--api.address tcp://0.0.0.0:1317', + '--grpc.address 0.0.0.0:9090', + '--evm-rpc.address 0.0.0.0:8545' + ].join(' '); + + const { stdout } = await execAsync(dockerCmd); + const containerId = stdout.trim(); + + return { + containerId, + containerName, + ports: { + rpc: rpcPort, + rest: restPort, + grpc: grpcPort, + evmRpc: evmRpcPort, + p2p: p2pPort + } + }; + } catch (error) { + throw new Error(`Failed to start Sei chain: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Stop a Sei chain Docker container + */ +export async function stopSeiChain(containerName: string): Promise { + try { + await execAsync(`docker stop ${containerName}`); + await execAsync(`docker rm ${containerName}`); + } catch (error) { + throw new Error(`Failed to stop Sei chain: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Get status of running Sei chain containers + */ +export async function getSeiChainStatus(): Promise { + try { + const { stdout } = await execAsync( + 'docker ps -a --filter "ancestor=ghcr.io/sei-protocol/sei" --format "table {{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}\\t{{.CreatedAt}}"' + ); + + const lines = stdout.trim().split('\n'); + if (lines.length <= 1) { + return []; + } + + // Skip header line and parse container info + return lines.slice(1).map((line) => { + const [id, name, image, status, ports, created] = line.split('\t'); + return { + id: id?.trim() || '', + name: name?.trim() || '', + image: image?.trim() || '', + status: status?.trim() || '', + ports: ports?.trim() || '', + created: created?.trim() || '' + }; + }); + } catch (error) { + throw new Error(`Failed to get Sei chain status: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Get logs from a Sei chain container + */ +export async function getSeiChainLogs(containerName: string, lines = 100): Promise { + try { + const { stdout } = await execAsync(`docker logs --tail ${lines} ${containerName}`); + return stdout; + } catch (error) { + throw new Error(`Failed to get Sei chain logs: ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/packages/mcp-server/src/docker/index.ts b/packages/mcp-server/src/docker/index.ts new file mode 100644 index 000000000..99512117e --- /dev/null +++ b/packages/mcp-server/src/docker/index.ts @@ -0,0 +1,2 @@ +// Export Docker chain management functionality +export { registerDockerTools } from './tools.js'; diff --git a/packages/mcp-server/src/docker/initialize.ts b/packages/mcp-server/src/docker/initialize.ts new file mode 100644 index 000000000..823d0f4f6 --- /dev/null +++ b/packages/mcp-server/src/docker/initialize.ts @@ -0,0 +1,86 @@ +import type Docker from 'dockerode'; + +/** + * Creates and starts a Sei blockchain Docker container with comprehensive initialization + */ +export async function createAndStartSeiContainer( + docker: Docker, + imageTag: string, + finalContainerName: string, + rpcPort: number, + restPort: number, + evmRpcPort: number, + grpcPort: number +) { + // Pull the Docker image if it doesn't exist + await new Promise((resolve, reject) => { + docker.pull(imageTag, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) { + reject(err); + return; + } + + docker.modem.followProgress(stream, (err: Error | null) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }); + + // Create and start the container with comprehensive initialization + const container = await docker.createContainer({ + Image: imageTag, + name: finalContainerName, + Cmd: [ + 'sh', + '-c', + `# Check if blockchain is already initialized +if [ ! -f ~/.sei/config/genesis.json ]; then + echo "Initializing new Sei blockchain..." + seid init demo --chain-id sei-chain && \ + seid keys add admin --keyring-backend test && \ + seid add-genesis-account $(seid keys show admin -a --keyring-backend test) 100000000000000000000usei,100000000000000000000uusdc,100000000000000000000uatom --keyring-backend test && \ + seid gentx admin 7000000000000000usei --chain-id sei-chain --keyring-backend test && \ + seid collect-gentxs && \ + sed -i 's/"max_deposit_period": "172800s"/"max_deposit_period": "60s"/g' ~/.sei/config/genesis.json && \ + sed -i 's/"voting_period": "172800s"/"voting_period": "30s"/g' ~/.sei/config/genesis.json && \ + sed -i 's/"expedited_voting_period": "86400s"/"expedited_voting_period": "10s"/g' ~/.sei/config/genesis.json && \ + sed -i 's/"vote_period": "5"/"vote_period": "2"/g' ~/.sei/config/genesis.json && \ + sed -i 's/"community_tax": "0.020000000000000000"/"community_tax": "0.000000000000000000"/g' ~/.sei/config/genesis.json && \ + sed -i 's/"max_gas": "-1"/"max_gas": "35000000"/g' ~/.sei/config/genesis.json && \ + seid config keyring-backend test + echo "Blockchain initialization complete." +else + echo "Blockchain already initialized, skipping setup..." + seid config keyring-backend test +fi +echo "Starting Sei blockchain..." +seid start --chain-id sei-chain` + ], + ExposedPorts: { + '26657/tcp': {}, + '1317/tcp': {}, + '8545/tcp': {}, + '9090/tcp': {} + }, + HostConfig: { + PortBindings: { + '26657/tcp': [{ HostPort: rpcPort.toString() }], + '1317/tcp': [{ HostPort: restPort.toString() }], + '8545/tcp': [{ HostPort: evmRpcPort.toString() }], + '9090/tcp': [{ HostPort: grpcPort.toString() }] + } + } + }); + + await container.start(); + const containerInfo = await container.inspect(); + + return { + container, + containerId: containerInfo.Id + }; +} diff --git a/packages/mcp-server/src/docker/releases.ts b/packages/mcp-server/src/docker/releases.ts new file mode 100644 index 000000000..da1379071 --- /dev/null +++ b/packages/mcp-server/src/docker/releases.ts @@ -0,0 +1,41 @@ +/** + * Sei Chain Release Management + * + * This module provides functionality to fetch available Sei chain releases + * from the GitHub repository. + */ + +export interface SeiRelease { + tag: string; + name: string; + published_at: string; + prerelease: boolean; + draft: boolean; + html_url: string; + tarball_url: string; + zipball_url: string; +} + +/** + * Get available Sei chain releases from GitHub + * Uses the GitHub API to fetch release information + */ +export async function getSeiReleases(): Promise { + try { + const response = await fetch('https://api.github.com/repos/sei-protocol/sei-chain/releases'); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + + const releases = (await response.json()) as SeiRelease[]; + + // Sort by published date (newest first) + return releases.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()); + } catch (error) { + // Fallback to hardcoded versions if API fails + console.warn('Failed to fetch releases from GitHub API, using fallback versions:', error); + + return []; + } +} diff --git a/packages/mcp-server/src/docker/tools.ts b/packages/mcp-server/src/docker/tools.ts new file mode 100644 index 000000000..02fd907e6 --- /dev/null +++ b/packages/mcp-server/src/docker/tools.ts @@ -0,0 +1,678 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import Docker from 'dockerode'; +import * as releases from './releases.js'; +import { createAndStartSeiContainer } from './initialize.js'; + +const docker = new Docker(); + +/** + * Register Sei chain release tools with the MCP server + * Only registers tools if SEI_DOCKER_ENABLED environment variable is set to 'true' + * + * @param server The MCP server instance + */ +export function registerDockerTools(server: McpServer) { + // Check if Docker tools are enabled via environment variable + if (process.env.SEI_DOCKER_ENABLED !== 'true') { + return; + } + // Get available Sei chain releases + server.tool( + 'get_sei_releases', + 'Get a list of available Sei chain releases from GitHub. Returns official releases with metadata like publication date and download links.', + {}, + async () => { + try { + const seiReleases = await releases.getSeiReleases(); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + releases: seiReleases, + totalReleases: seiReleases.length, + latestRelease: seiReleases[0]?.tag || 'unknown', + repository: 'https://github.com/sei-protocol/sei-chain' + }, + null, + 2 + ) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error fetching Sei releases: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); + + // Start Sei chain container + server.tool( + 'start_sei_chain', + 'Start a Sei chain Docker container for a specific version. This will pull the image if needed and start a local Sei blockchain node.', + { + version: z.string().optional().describe("The Sei version to run (e.g., 'v6.1.0', 'latest'). Defaults to 'latest'"), + containerName: z.string().optional().describe("Custom name for the Docker container. Defaults to 'sei-chain-{version}'"), + rpcPort: z.number().optional().describe('Port for RPC endpoint (default: 26657)'), + restPort: z.number().optional().describe('Port for REST API endpoint (default: 1317)'), + evmRpcPort: z.number().optional().describe('Port for EVM RPC endpoint (default: 8545)'), + grpcPort: z.number().optional().describe('Port for gRPC endpoint (default: 9090)') + }, + async ({ version = 'latest', containerName, rpcPort = 26657, restPort = 1317, evmRpcPort = 8545, grpcPort = 9090 }) => { + try { + // Generate container name if not provided + const finalContainerName = containerName || `sei-chain-${version.replace(/[^a-zA-Z0-9]/g, '-')}`; + + // Stop and remove existing container if it exists + try { + const existingContainer = docker.getContainer(finalContainerName); + await existingContainer.stop(); + await existingContainer.remove(); + } catch (error) { + // Ignore errors when stopping/removing non-existent containers + } + + // Pull the image and create/start the container + const imageTag = `ghcr.io/sei-protocol/sei:${version}`; + const { container, containerId } = await createAndStartSeiContainer( + docker, + imageTag, + finalContainerName, + rpcPort, + restPort, + evmRpcPort, + grpcPort + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + version, + containerId, + containerName: finalContainerName, + ports: { + rpc: rpcPort, + rest: restPort, + evmRpc: evmRpcPort, + grpc: grpcPort + }, + endpoints: { + rpc: `http://localhost:${rpcPort}`, + rest: `http://localhost:${restPort}`, + evmRpc: `http://localhost:${evmRpcPort}`, + grpc: `http://localhost:${grpcPort}` + }, + dockerImage: imageTag, + message: 'Sei chain started successfully' + }, + null, + 2 + ) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error starting Sei chain: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); + + // Get running Sei chain containers + server.tool( + 'get_running_chains', + 'Get a list of currently running Sei chain Docker containers. Shows container names, status, ports, and runtime information.', + {}, + async () => { + try { + const containers = await docker.listContainers({ all: false }); // Only running containers + + // Filter for Sei chain containers + const seiContainers = containers.filter(container => + container.Names.some(name => name.includes('sei-chain')) || + container.Image.includes('sei-protocol/sei') || + container.Image.includes('ghcr.io/sei-protocol/sei') + ); + + const containerDetails = await Promise.all( + seiContainers.map(async (container) => { + try { + const containerObj = docker.getContainer(container.Id); + const inspectData = await containerObj.inspect(); + + // Extract port mappings + const portMappings: Record = {}; + if (inspectData.NetworkSettings.Ports) { + for (const [containerPort, hostBindings] of Object.entries(inspectData.NetworkSettings.Ports)) { + if (hostBindings && hostBindings.length > 0) { + const hostPort = hostBindings[0].HostPort; + const portName = containerPort.replace('/tcp', ''); + + // Map common Sei ports to their purposes + if (portName === '26657') portMappings.rpc = `http://localhost:${hostPort}`; + else if (portName === '1317') portMappings.rest = `http://localhost:${hostPort}`; + else if (portName === '8545') portMappings.evmRpc = `http://localhost:${hostPort}`; + else portMappings[portName] = `http://localhost:${hostPort}`; + } + } + } + + return { + id: container.Id.substring(0, 12), + name: container.Names[0].replace('/', ''), + image: container.Image, + status: container.Status, + state: container.State, + created: new Date(container.Created * 1000).toISOString(), + ports: portMappings, + uptime: inspectData.State.StartedAt ? + Math.floor((Date.now() - new Date(inspectData.State.StartedAt).getTime()) / 1000) : 0 + }; + } catch (error) { + return { + id: container.Id.substring(0, 12), + name: container.Names[0].replace('/', ''), + image: container.Image, + status: container.Status, + state: container.State, + created: new Date(container.Created * 1000).toISOString(), + ports: {}, + uptime: 0, + error: error instanceof Error ? error.message : String(error) + }; + } + }) + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + totalContainers: containerDetails.length, + containers: containerDetails, + message: containerDetails.length > 0 + ? `Found ${containerDetails.length} running Sei chain container(s)` + : 'No running Sei chain containers found' + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting running chains: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); + + // Restart Sei chain container + server.tool( + 'restart_sei_container', + 'Restart a stopped Sei chain Docker container. This will start an existing container without re-initializing the blockchain data.', + { + containerName: z.string().optional().describe("Name of the container to restart. If not provided, will show all available stopped Sei containers") + }, + async ({ containerName }) => { + try { + const containers = await docker.listContainers({ all: true }); + let targetContainers: Array<{ id: string; name: string; status: string; state: string }> = []; + + if (containerName) { + // Restart specific container + const container = containers.find(c => + c.Names.some(name => name.replace('/', '') === containerName) + ); + + if (!container) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + message: `Container '${containerName}' not found` + }, null, 2) + } + ] + }; + } + + targetContainers = [{ + id: container.Id, + name: containerName, + status: container.Status, + state: container.State + }]; + } else { + // Find all stopped Sei chain containers + const seiContainers = containers.filter(container => + (container.Names.some(name => name.includes('sei-chain')) || + container.Image.includes('sei-protocol/sei') || + container.Image.includes('ghcr.io/sei-protocol/sei')) && + container.State !== 'running' + ); + + if (seiContainers.length === 0) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + message: 'No stopped Sei chain containers found to restart' + }, null, 2) + } + ] + }; + } + + targetContainers = seiContainers.map(container => ({ + id: container.Id, + name: container.Names[0].replace('/', ''), + status: container.Status, + state: container.State + })); + } + + const results: Array<{ + container: string; + restarted: boolean; + status: 'success' | 'error'; + error?: string; + previousState?: string; + ports?: Record; + }> = []; + + for (const containerInfo of targetContainers) { + try { + const container = docker.getContainer(containerInfo.id); + const inspectData = await container.inspect(); + + // Check if container is already running + if (inspectData.State.Running) { + results.push({ + container: containerInfo.name, + restarted: false, + status: 'error', + error: 'Container is already running', + previousState: containerInfo.state + }); + continue; + } + + // Start the container + await container.start(); + + // Get updated container info including port mappings + const updatedInspectData = await container.inspect(); + const portMappings: Record = {}; + + if (updatedInspectData.NetworkSettings.Ports) { + for (const [containerPort, hostBindings] of Object.entries(updatedInspectData.NetworkSettings.Ports)) { + if (hostBindings && hostBindings.length > 0) { + const hostPort = hostBindings[0].HostPort; + const portName = containerPort.replace('/tcp', ''); + + // Map common Sei ports to their purposes + if (portName === '26657') portMappings.rpc = `http://localhost:${hostPort}`; + else if (portName === '1317') portMappings.rest = `http://localhost:${hostPort}`; + else if (portName === '8545') portMappings.evmRpc = `http://localhost:${hostPort}`; + else if (portName === '9090') portMappings.grpc = `http://localhost:${hostPort}`; + else portMappings[portName] = `http://localhost:${hostPort}`; + } + } + } + + results.push({ + container: containerInfo.name, + restarted: true, + status: 'success', + previousState: containerInfo.state, + ports: portMappings + }); + } catch (error) { + results.push({ + container: containerInfo.name, + restarted: false, + status: 'error', + error: error instanceof Error ? error.message : String(error), + previousState: containerInfo.state + }); + } + } + + const successCount = results.filter(r => r.status === 'success').length; + const errorCount = results.filter(r => r.status === 'error').length; + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: errorCount === 0, + message: `Processed ${results.length} container(s): ${successCount} restarted, ${errorCount} failed`, + containers: results, + summary: { + total: results.length, + restarted: successCount, + failed: errorCount + } + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error restarting containers: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); + + // Delete Sei chain container + server.tool( + 'delete_sei_container', + 'Delete (remove) one or more Sei chain Docker containers. This will permanently remove the container and its data. The container must be stopped first.', + { + containerName: z.string().optional().describe("Name of the specific container to delete. If not provided, will show all available Sei containers for selection"), + force: z.boolean().optional().describe('Force removal of running containers (stops them first, default: false)'), + removeVolumes: z.boolean().optional().describe('Remove associated volumes with the container (default: false)') + }, + async ({ containerName, force = false, removeVolumes = false }) => { + try { + const containers = await docker.listContainers({ all: true }); + let targetContainers: Array<{ id: string; name: string; status: string; state: string }> = []; + + if (containerName) { + // Delete specific container + const container = containers.find(c => + c.Names.some(name => name.replace('/', '') === containerName) + ); + + if (!container) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + message: `Container '${containerName}' not found` + }, null, 2) + } + ] + }; + } + + targetContainers = [{ + id: container.Id, + name: containerName, + status: container.Status, + state: container.State + }]; + } else { + // Find all sei-chain containers + const seiContainers = containers.filter(container => + container.Names.some(name => name.includes('sei-chain')) || + container.Image.includes('sei-protocol/sei') || + container.Image.includes('ghcr.io/sei-protocol/sei') + ); + + if (seiContainers.length === 0) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + message: 'No Sei chain containers found to delete' + }, null, 2) + } + ] + }; + } + + targetContainers = seiContainers.map(container => ({ + id: container.Id, + name: container.Names[0].replace('/', ''), + status: container.Status, + state: container.State + })); + } + + const results: Array<{ + container: string; + deleted: boolean; + status: 'success' | 'error'; + error?: string; + volumesRemoved?: boolean; + wasRunning?: boolean; + }> = []; + for (const containerInfo of targetContainers) { + try { + const container = docker.getContainer(containerInfo.id); + const inspectData = await container.inspect(); + + // Stop container if it's running and force is enabled + if (inspectData.State.Running) { + if (force) { + await container.stop(); + } else { + results.push({ + container: containerInfo.name, + deleted: false, + status: 'error', + error: 'Container is running. Use force=true to stop and remove, or stop it first.' + }); + continue; + } + } + + // Remove the container + await container.remove({ + v: removeVolumes, // Remove volumes if requested + force: force + }); + + results.push({ + container: containerInfo.name, + deleted: true, + status: 'success', + volumesRemoved: removeVolumes, + wasRunning: inspectData.State.Running + }); + } catch (error) { + results.push({ + container: containerInfo.name, + deleted: false, + status: 'error', + error: error instanceof Error ? error.message : String(error) + }); + } + } + + const successCount = results.filter(r => r.status === 'success').length; + const errorCount = results.filter(r => r.status === 'error').length; + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: errorCount === 0, + message: `Processed ${results.length} container(s): ${successCount} deleted, ${errorCount} failed`, + containers: results, + summary: { + total: results.length, + deleted: successCount, + failed: errorCount, + options: { + force, + removeVolumes + } + } + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error deleting containers: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); + + // Stop Sei chain container + server.tool( + 'stop_sei_chain', + 'Stop and remove a running Sei chain Docker container. This will gracefully stop the blockchain node and clean up the container.', + { + containerName: z.string().optional().describe("Name of the container to stop. If not provided, will attempt to stop containers matching 'sei-chain-*' pattern"), + removeContainer: z.boolean().optional().describe('Whether to remove the container after stopping (default: false)') + }, + async ({ containerName, removeContainer = false }) => { + try { + const containers = await docker.listContainers({ all: true }); + let targetContainers: string[] = []; + + if (containerName) { + // Stop specific container + targetContainers = [containerName]; + } else { + // Find all sei-chain containers + targetContainers = containers + .filter(container => + container.Names.some(name => name.includes('sei-chain')) + ) + .map(container => container.Names[0].replace('/', '')); + } + + if (targetContainers.length === 0) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + message: containerName + ? `Container '${containerName}' not found` + : 'No Sei chain containers found' + }, null, 2) + } + ] + }; + } + + const results: Array<{ + container: string; + stopped: boolean; + removed: boolean; + status: 'success' | 'error'; + error?: string; + }> = []; + for (const name of targetContainers) { + try { + const container = docker.getContainer(name); + const containerInfo = await container.inspect(); + + if (containerInfo.State.Running) { + await container.stop(); + } + + if (removeContainer) { + await container.remove(); + } + + results.push({ + container: name, + stopped: true, + removed: removeContainer, + status: 'success' + }); + } catch (error) { + results.push({ + container: name, + stopped: false, + removed: false, + status: 'error', + error: error instanceof Error ? error.message : String(error) + }); + } + } + + const successCount = results.filter(r => r.status === 'success').length; + const errorCount = results.filter(r => r.status === 'error').length; + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: errorCount === 0, + message: `Processed ${results.length} container(s): ${successCount} successful, ${errorCount} failed`, + containers: results, + summary: { + total: results.length, + successful: successCount, + failed: errorCount + } + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error stopping Sei chain: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); +} diff --git a/packages/mcp-server/src/server/server.ts b/packages/mcp-server/src/server/server.ts index de6c0f0d1..72d491a6b 100644 --- a/packages/mcp-server/src/server/server.ts +++ b/packages/mcp-server/src/server/server.ts @@ -6,6 +6,7 @@ import { createSeiJSDocsSearchTool } from '../mintlify/search.js'; import { getPackageInfo } from './package-info.js'; import { getSupportedNetworks } from '../core/chains.js'; import { createDocsSearchTool } from '../docs/index.js'; +import { registerDockerTools } from '../docker/index.js'; export const getServer = async () => { try { @@ -18,6 +19,7 @@ export const getServer = async () => { registerEVMResources(server); registerEVMTools(server); registerEVMPrompts(server); + registerDockerTools(server); await createSeiJSDocsSearchTool(server); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e5523b7a..58ced7556 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,6 +112,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + dockerode: + specifier: ^4.0.2 + version: 4.0.7 dotenv: specifier: ^16.5.0 version: 16.6.1 @@ -140,6 +143,9 @@ importers: '@types/cors': specifier: ^2.8.17 version: 2.8.19 + '@types/dockerode': + specifier: ^3.3.31 + version: 3.3.43 '@types/express': specifier: ^5.0.0 version: 5.0.3 @@ -387,6 +393,9 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -844,6 +853,15 @@ packages: '@ethersproject/web@5.8.0': resolution: {integrity: sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==} + '@grpc/grpc-js@1.13.4': + resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -1258,6 +1276,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} engines: {node: '>= 10.16.0'} @@ -1704,6 +1725,12 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@3.3.43': + resolution: {integrity: sha512-YCi0aKKpKeC9dhKTbuglvsWDnAyuIITd6CCJSTKiAdbDzPH4RWu0P9IK2XkJHdyplH6mzYtDYO+gB06JlzcPxg==} + '@types/es-aggregate-error@1.0.6': resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -1770,6 +1797,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.124': + resolution: {integrity: sha512-hY4YWZFLs3ku6D2Gqo3RchTd9VRCcrjqp/I0mmohYeUVA5Y8eCXKJEasHxLAJVZRJuQogfd1GiJ9lgogBgKeuQ==} + '@types/node@20.19.12': resolution: {integrity: sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==} @@ -1794,6 +1824,9 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -2139,6 +2172,9 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + asn1js@3.0.6: resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} engines: {node: '>=12.0.0'} @@ -2274,6 +2310,9 @@ packages: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bech32@1.1.4: resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} @@ -2366,6 +2405,10 @@ packages: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} + buildcheck@0.0.6: + resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} + engines: {node: '>=10.0.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2627,6 +2670,10 @@ packages: cosmjs-types@0.9.0: resolution: {integrity: sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==} + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2815,6 +2862,14 @@ packages: resolution: {integrity: sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg==} engines: {node: '>=6'} + docker-modem@5.0.6: + resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} + engines: {node: '>= 8.0'} + + dockerode@4.0.7: + resolution: {integrity: sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA==} + engines: {node: '>= 8.0'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -4149,6 +4204,9 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -4168,6 +4226,9 @@ packages: long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4527,6 +4588,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.23.0: + resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4933,6 +4997,10 @@ packages: resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} hasBin: true + 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'} @@ -5409,9 +5477,16 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -5702,6 +5777,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -5759,6 +5837,9 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -5873,6 +5954,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -6372,6 +6457,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@balena/dockerignore@1.0.2': {} + '@bcoe/v8-coverage@0.2.3': {} '@biomejs/biome@1.9.4': @@ -7024,6 +7111,18 @@ snapshots: '@ethersproject/properties': 5.8.0 '@ethersproject/strings': 5.8.0 + '@grpc/grpc-js@1.13.4': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@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 + '@hexagon/base64@1.1.28': {} '@img/sharp-darwin-arm64@0.33.5': @@ -7638,6 +7737,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': dependencies: jsep: 1.4.0 @@ -8543,6 +8644,17 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.18.0 + '@types/ssh2': 1.15.5 + + '@types/dockerode@3.3.43': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.18.0 + '@types/ssh2': 1.15.5 + '@types/es-aggregate-error@1.0.6': dependencies: '@types/node': 22.18.0 @@ -8615,6 +8727,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@18.19.124': + dependencies: + undici-types: 5.26.5 + '@types/node@20.19.12': dependencies: undici-types: 6.21.0 @@ -8646,6 +8762,10 @@ snapshots: '@types/node': 22.18.0 '@types/send': 0.17.5 + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.124 + '@types/stack-utils@2.0.3': {} '@types/unist@2.0.11': {} @@ -8929,6 +9049,10 @@ snapshots: arrify@1.0.1: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + asn1js@3.0.6: dependencies: pvtsutils: 1.3.6 @@ -9100,6 +9224,10 @@ snapshots: basic-ftp@5.0.5: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + bech32@1.1.4: {} better-opn@3.0.2: @@ -9233,6 +9361,9 @@ snapshots: node-gyp-build: 4.8.4 optional: true + buildcheck@0.0.6: + optional: true + bytes@3.1.2: {} cacheable-lookup@7.0.0: {} @@ -9453,6 +9584,12 @@ snapshots: cosmjs-types@0.9.0: {} + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.6 + nan: 2.23.0 + optional: true + create-jest@29.7.0(@types/node@20.19.12)(ts-node@10.9.2(@types/node@20.19.12)(typescript@5.9.2)): dependencies: '@jest/types': 29.6.3 @@ -9622,6 +9759,27 @@ snapshots: dependencies: dns-packet: 5.6.1 + docker-modem@5.0.6: + dependencies: + debug: 4.4.1 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.7: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.13.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.6 + protobufjs: 7.5.4 + tar-fs: 2.1.3 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -11773,6 +11931,8 @@ snapshots: dependencies: p-locate: 4.1.0 + lodash.camelcase@4.3.0: {} + lodash.memoize@4.1.2: {} lodash.startcase@4.4.0: {} @@ -11788,6 +11948,8 @@ snapshots: long@4.0.0: {} + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -12404,6 +12566,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.23.0: + optional: true + nanoid@3.3.11: {} napi-build-utils@2.0.0: {} @@ -12809,6 +12974,21 @@ snapshots: '@types/node': 22.18.0 long: 4.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': 22.18.0 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -13528,8 +13708,18 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + split-ca@1.0.1: {} + sprintf-js@1.0.3: {} + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.23.0 + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -13918,6 +14108,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tweetnacl@0.14.5: {} + type-detect@4.0.8: {} type-fest@0.21.3: {} @@ -13987,6 +14179,8 @@ snapshots: buffer: 5.7.1 through: 2.3.8 + undici-types@5.26.5: {} + undici-types@6.19.8: {} undici-types@6.21.0: {} @@ -14147,6 +14341,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + uuid@8.3.2: {} v8-compile-cache-lib@3.0.1: {} From 45d7e943e729c56a862e69da152bc8686ce96489 Mon Sep 17 00:00:00 2001 From: carson Date: Mon, 8 Sep 2025 09:29:27 -0700 Subject: [PATCH 2/5] Add docs for docker tools --- docs/mcp-server/environment-variables.mdx | 63 ++++++ docs/mcp-server/introduction.mdx | 11 +- docs/mcp-server/quickstart.mdx | 15 +- docs/mcp-server/tools.mdx | 45 +++- docs/mcp-server/troubleshooting.mdx | 98 +++++++++ package.json | 4 +- pnpm-lock.yaml | 252 ++++++++++++++++++++++ 7 files changed, 482 insertions(+), 6 deletions(-) diff --git a/docs/mcp-server/environment-variables.mdx b/docs/mcp-server/environment-variables.mdx index fe316a1f1..d97cb0ba0 100644 --- a/docs/mcp-server/environment-variables.mdx +++ b/docs/mcp-server/environment-variables.mdx @@ -125,6 +125,21 @@ The Sei MCP Server supports configuration through environment variables, which c - **Description**: Base path for HTTP endpoints - **Format**: Must start with `/` (automatically normalized) +### Docker Configuration + +#### `SEI_DOCKER_ENABLED` +- **Type**: `string` +- **Default**: `"false"` +- **Description**: Enable Docker tools for local Sei blockchain development +- **Options**: + - `"false"` - Docker tools disabled (default) + - `"true"` - Enable Docker container management tools +- **Prerequisites**: Docker must be installed and running on your system + + +**Docker Tools**: When enabled, provides 6 additional tools for managing local Sei blockchain containers including start, stop, restart, and cleanup operations. + + ### System Configuration #### `PATH` @@ -248,6 +263,48 @@ The Sei MCP Server supports configuration through environment variables, which c **Status**: Deprecated - migrate to streamable-http for better performance + + + Enable Docker tools for local blockchain development: + + + + ```json + { + "mcpServers": { + "sei": { + "command": "npx", + "args": ["-y", "@sei-js/mcp-server"], + "env": { + "SEI_DOCKER_ENABLED": "true", + "WALLET_MODE": "private-key", + "PRIVATE_KEY": "0x123..." + } + } + } + } + ``` + + + ```bash + # Enable Docker tools with wallet support + SEI_DOCKER_ENABLED=true \ + WALLET_MODE=private-key \ + PRIVATE_KEY=0x123... \ + npx @sei-js/mcp-server + + # Or with Claude CLI + claude mcp add sei-docker npx @sei-js/mcp-server \ + --env SEI_DOCKER_ENABLED=true \ + --env WALLET_MODE=private-key \ + --env PRIVATE_KEY=0x123... + ``` + + + + **Use Case**: Local development, testing, and blockchain experimentation + **Tools Added**: get_sei_releases, start_sei_chain, get_running_chains, restart_sei_container, stop_sei_chain, delete_sei_container + ## Security Best Practices @@ -286,6 +343,12 @@ The Sei MCP Server supports configuration through environment variables, which c - Verify JSON syntax in MCP configuration - Check for typos in variable names +**Docker Tools Not Available** +- Ensure Docker is installed and running: `docker --version` +- Verify `SEI_DOCKER_ENABLED=true` is set correctly +- Check Docker daemon is accessible: `docker ps` +- Restart MCP server after enabling Docker tools + Check our comprehensive troubleshooting guide for solutions to specific problems. diff --git a/docs/mcp-server/introduction.mdx b/docs/mcp-server/introduction.mdx index 90ddb6724..066a7449a 100644 --- a/docs/mcp-server/introduction.mdx +++ b/docs/mcp-server/introduction.mdx @@ -97,8 +97,9 @@ icon: "robot" "SERVER_PATH": "/mcp", "MAINNET_RPC_URL": "https://evm-rpc.sei-apis.com", "TESTNET_RPC_URL": "https://evm-rpc-testnet.sei-apis.com", - "DEVNET_RPC_URL": "https://evm-rpc-arctic-1.sei-apis.com" - } + "DEVNET_RPC_URL": "https://evm-rpc-arctic-1.sei-apis.com", + "SEI_DOCKER_ENABLED": "true", + } } } } @@ -113,6 +114,7 @@ icon: "robot" The Model Context Protocol is an open standard that connects AI systems with custom prompts, tools and data sources (context). It enables things like: - **Real-time blockchain data access** - Get current balances, transaction history, and network status directly from Sei +- **Local development environment** - Spin up complete Sei blockchain containers for testing and development - **Full execution and write operations** - Deploy contracts, execute transactions, and interact with smart contracts (with wallet configured) - **Up-to-date documentation access** - Search both main Sei docs and Sei-JS package documentation for comprehensive guidance - **Specialized blockchain capabilities** - Access Sei-specific features like precompiles and native token operations @@ -136,7 +138,7 @@ The Sei MCP Server leverages this protocol to bring all of this and more directl - Access 29 blockchain tools covering token management, NFT operations, smart contract interactions, network monitoring, and documentation search. + Access 35+ blockchain tools covering token management, NFT operations, smart contract interactions, network monitoring, local development with Docker, and documentation search. @@ -148,6 +150,9 @@ The Sei MCP Server leverages this protocol to bring all of this and more directl "Generate a boilerplate dApp with Sei Global Wallet connection" "What's my SEI balance?" "Send 1 SEI to this address" +"Start a local Sei blockchain for testing" +"Show me all running Sei containers" +"Stop my local blockchain and clean up" ``` ## Next Steps diff --git a/docs/mcp-server/quickstart.mdx b/docs/mcp-server/quickstart.mdx index 0e845ef8b..e222fb2af 100644 --- a/docs/mcp-server/quickstart.mdx +++ b/docs/mcp-server/quickstart.mdx @@ -126,6 +126,9 @@ icon: "download" # Wallet-enabled mode claude mcp add sei-mcp-server-wallet npx @sei-js/mcp-server --env WALLET_MODE=private-key PRIVATE_KEY=0x123... + + # Docker-enabled mode for local development + claude mcp add sei-mcp-server-docker npx @sei-js/mcp-server --env SEI_DOCKER_ENABLED=true WALLET_MODE=private-key PRIVATE_KEY=0x123... ``` @@ -233,6 +236,16 @@ After setup, verify the server is working by asking your AI assistant: This confirms wallet tools are enabled. + + + If you enabled Docker tools with `SEI_DOCKER_ENABLED=true`: + + **Ask:** "What Sei chain versions are available?" + + **Ask:** "Start a local Sei blockchain" + + This confirms Docker tools are working. + ## Next Steps @@ -242,7 +255,7 @@ After setup, verify the server is working by asking your AI assistant: Configure wallet access and custom RPC endpoints - Explore 50+ blockchain tools and capabilities + Explore 35+ blockchain tools and capabilities Solutions to common setup and configuration issues diff --git a/docs/mcp-server/tools.mdx b/docs/mcp-server/tools.mdx index eed895699..70e066184 100644 --- a/docs/mcp-server/tools.mdx +++ b/docs/mcp-server/tools.mdx @@ -4,7 +4,7 @@ description: "Complete reference of all Sei MCP server tools" icon: "wrench" --- -The Sei MCP Server provides various tools for blockchain operations, queries, documentation lookup, and more. **By default, wallet tools are disabled** and only read-only tools are available. [Enable wallet tools](/mcp-server/quickstart#wallet-connection) to unlock transaction capabilities. +The Sei MCP Server provides various tools for blockchain operations, queries, documentation lookup, local development, and more. **By default, wallet tools are disabled** and only read-only tools are available. [Enable wallet tools](/mcp-server/quickstart#wallet-connection) to unlock transaction capabilities. **Wallet Tools Disabled by Default** @@ -116,6 +116,49 @@ These tools require [wallet configuration](/mcp-server/quickstart#wallet-connect | `search_docs` | Search the main Sei docs for general chain information, ecosystem providers, and user onboarding guides | "How do I bridge tokens to Sei?" | | `search_sei_js_docs` | Search Sei-JS documentation | "How do I use precompiles with Viem?" | +## Local Development (Docker Tools) 🐳 + + +**Docker Required**: These tools require Docker to be installed and running on your system. Enable with `SEI_DOCKER_ENABLED=true`. + + +Manage local Sei blockchain containers for development and testing: + +| Tool | Purpose | Example Usage | +|------|---------|---------------| +| `get_sei_releases` | Get available Sei chain releases | "What Sei versions are available?" | +| `start_sei_chain` | Start local Sei blockchain container | "Start a local Sei chain with version v6.1.0" | +| `get_running_chains` | List running Sei containers | "Show me all running Sei containers" | +| `restart_sei_container` | Restart stopped Sei container | "Restart my sei-chain-latest container" | +| `stop_sei_chain` | Stop running Sei container | "Stop my local blockchain" | +| `delete_sei_container` | Delete Sei container | "Delete the sei-chain-v6.1.0 container" | + +### Docker Tool Features + +- **Multi-version support**: Run different Sei chain versions simultaneously +- **Pre-configured accounts**: Test accounts with funds automatically created +- **Port management**: Configurable RPC, REST, EVM, and gRPC ports +- **Container lifecycle**: Full start, stop, restart, and cleanup operations +- **GitHub integration**: Fetch latest releases directly from sei-protocol/sei-chain + +### Enabling Docker Tools + +To use Docker tools, add the environment variable to your MCP configuration: + +```json +{ + "mcpServers": { + "sei": { + "command": "npx", + "args": ["-y", "@sei-js/mcp-server"], + "env": { + "SEI_DOCKER_ENABLED": "true" + } + } + } +} +``` + ## Enabling Wallet Tools To use wallet-required tools (🔐), you must configure a wallet connection: diff --git a/docs/mcp-server/troubleshooting.mdx b/docs/mcp-server/troubleshooting.mdx index 70640bd68..79b2abb3b 100644 --- a/docs/mcp-server/troubleshooting.mdx +++ b/docs/mcp-server/troubleshooting.mdx @@ -147,7 +147,92 @@ Many issues stem from incorrect environment variable configuration. +### Docker Issues +Problems with Docker tools and local blockchain containers. + + + + If Docker tools aren't showing up or you get "Docker not available" errors: + + ```bash + # Check if Docker is installed + docker --version + + # Check if Docker daemon is running + docker ps + + # Verify SEI_DOCKER_ENABLED is set + echo $SEI_DOCKER_ENABLED + ``` + + **Solutions:** + - Install Docker from [docker.com](https://docker.com) + - Start Docker Desktop or Docker daemon + - Ensure `SEI_DOCKER_ENABLED=true` in your MCP configuration + - Restart your AI assistant after enabling Docker tools + + + + If Sei containers fail to start or crash immediately: + + ```bash + # Check Docker logs + docker logs sei-chain-latest + + # Check available system resources + docker system df + + # Check port conflicts + lsof -i :26657 -i :1317 -i :8545 -i :9090 + ``` + + **Common causes:** + - Port conflicts (26657, 1317, 8545, 9090 already in use) + - Insufficient disk space or memory + - Docker image corruption + + **Solutions:** + - Stop conflicting services or use different ports + - Free up disk space (`docker system prune`) + - Pull fresh Docker image (`docker pull ghcr.io/sei-protocol/sei:latest`) + + + + If Docker images fail to download: + + ```bash + # Test Docker Hub connectivity + docker pull hello-world + + # Try pulling Sei image manually + docker pull ghcr.io/sei-protocol/sei:latest + ``` + + **Solutions:** + - Check internet connection + - Verify Docker Hub/GitHub Container Registry access + - Try different network (corporate firewalls may block) + - Use VPN if in restricted region + + + + If containers won't stop, restart, or delete: + + ```bash + # Force stop container + docker stop sei-chain-latest --time 0 + + # Force remove container + docker rm sei-chain-latest --force + + # Clean up all stopped containers + docker container prune + ``` + + **Use with caution:** Force operations may cause data loss. + + ## Error Reference @@ -168,6 +253,18 @@ Many issues stem from incorrect environment variable configuration. **Cause**: Invalid RPC URL or network connectivity issues **Solution**: Check your RPC URLs and internet connection + + **Cause**: Docker not installed, not running, or SEI_DOCKER_ENABLED not set + **Solution**: Install Docker, start daemon, and set SEI_DOCKER_ENABLED=true + + + **Cause**: Required ports (26657, 1317, 8545, 9090) are occupied by other services + **Solution**: Stop conflicting services or configure different ports + + + **Cause**: Docker image issues, resource constraints, or configuration problems + **Solution**: Check Docker logs, free up resources, or pull fresh image + ## Getting Help @@ -188,6 +285,7 @@ If you're still experiencing problems, please [create an issue on GitHub](https: Include: - Operating system and version - Node.js version (`node --version`) + - Docker version (`docker --version`) if using Docker tools - MCP client (Cursor, Claude Desktop, etc.) - Your PATH (`echo $PATH`) diff --git a/package.json b/package.json index f839277c5..0cf05b7d2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ }, "dependencies": { "@biomejs/biome": "^1.9.4", - "@changesets/cli": "^2.28.1" + "@changesets/cli": "^2.28.1", + "optional": "^0.1.4", + "sharp": "^0.34.3" }, "devDependencies": { "@types/jest": "^29.5.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58ced7556..7c47d9d1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@changesets/cli': specifier: ^2.28.1 version: 2.29.6(@types/node@22.18.0) + optional: + specifier: ^0.1.4 + version: 0.1.4 + sharp: + specifier: ^0.34.3 + version: 0.34.3 devDependencies: '@types/jest': specifier: ^29.5.14 @@ -871,105 +877,227 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/checkbox@4.2.2': resolution: {integrity: sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==} engines: {node: '>=18'} @@ -4742,6 +4870,9 @@ packages: openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optional@0.1.4: + resolution: {integrity: sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -5364,6 +5495,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -7130,76 +7265,162 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.5.0 optional: true + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.3': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.3': + optional: true + '@inquirer/checkbox@4.2.2(@types/node@22.18.0)': dependencies: '@inquirer/core': 10.2.0(@types/node@22.18.0) @@ -12700,6 +12921,8 @@ snapshots: openapi-types@12.1.3: {} + optional@0.1.4: {} + ora@5.4.1: dependencies: bl: 4.1.0 @@ -13560,6 +13783,35 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 From 8c43663c3207fe0a243f2b88c82d22be4825c409 Mon Sep 17 00:00:00 2001 From: carson Date: Mon, 8 Sep 2025 09:41:44 -0700 Subject: [PATCH 3/5] Switched to dockerode and fetch --- .../mcp-server/src/core/services/chain.ts | 199 +++++++++--------- 1 file changed, 100 insertions(+), 99 deletions(-) diff --git a/packages/mcp-server/src/core/services/chain.ts b/packages/mcp-server/src/core/services/chain.ts index 60e9609a9..f57c4e8f4 100644 --- a/packages/mcp-server/src/core/services/chain.ts +++ b/packages/mcp-server/src/core/services/chain.ts @@ -1,7 +1,6 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; +import Docker from 'dockerode'; -const execAsync = promisify(exec); +const docker = new Docker(); /** * Interface for Sei chain version information @@ -27,69 +26,40 @@ export interface DockerContainer { /** * Get available Sei chain versions from GitHub Container Registry - * Uses skopeo to query the registry without pulling images + * Uses GitHub Container Registry API to query available tags */ export async function getAvailableSeiVersions(): Promise { try { - // First try using skopeo (more reliable for registry queries) - try { - const { stdout } = await execAsync('skopeo list-tags docker://ghcr.io/sei-protocol/sei'); - const data = JSON.parse(stdout); - - // Convert tags to version objects with additional metadata - const versions: SeiChainVersion[] = []; - - for (const tag of data.Tags || []) { - try { - // Get image details for each tag - const { stdout: inspectOutput } = await execAsync(`skopeo inspect docker://ghcr.io/sei-protocol/sei:${tag}`); - const imageInfo = JSON.parse(inspectOutput); - - versions.push({ - tag, - digest: imageInfo.Digest || 'unknown', - created: imageInfo.Created || 'unknown', - size: imageInfo.Size ? `${Math.round(imageInfo.Size / 1024 / 1024)}MB` : 'unknown' - }); - } catch (error) { - // If we can't get details for a specific tag, still include it - versions.push({ - tag, - digest: 'unknown', - created: 'unknown', - size: 'unknown' - }); - } - } - - return versions.sort((a, b) => b.tag.localeCompare(a.tag, undefined, { numeric: true })); - } catch (skopeoError) { - // Fallback to docker registry API - console.warn('Skopeo not available, falling back to registry API'); - return await getVersionsFromRegistryAPI(); - } + // Use GitHub Container Registry API directly + return await getVersionsFromRegistryAPI(); } catch (error) { throw new Error(`Failed to get available Sei versions: ${error instanceof Error ? error.message : String(error)}`); } } /** - * Fallback method using GitHub Container Registry API + * Get versions using GitHub Container Registry API with fetch */ async function getVersionsFromRegistryAPI(): Promise { try { - // Use curl to query the GitHub Container Registry API - const { stdout } = await execAsync( - 'curl -s "https://ghcr.io/v2/sei-protocol/sei/tags/list" -H "Accept: application/vnd.docker.distribution.manifest.v2+json"' - ); + // Use fetch to query the GitHub Container Registry API + const response = await fetch('https://ghcr.io/v2/sei-protocol/sei/tags/list', { + headers: { + 'Accept': 'application/vnd.docker.distribution.manifest.v2+json' + } + }); - const data = JSON.parse(stdout); + if (!response.ok) { + throw new Error(`Registry API returned ${response.status}: ${response.statusText}`); + } + + const data = await response.json() as { tags?: string[] }; - if (!data.tags) { + if (!data.tags || !Array.isArray(data.tags)) { throw new Error('No tags found in registry response'); } - // Convert to version objects (without detailed metadata in fallback) + // Convert to version objects (without detailed metadata) const versions: SeiChainVersion[] = data.tags.map((tag: string) => ({ tag, digest: 'unknown', @@ -177,8 +147,9 @@ export async function startSeiChain( // Stop existing container if it exists try { - await execAsync(`docker stop ${containerName} 2>/dev/null || true`); - await execAsync(`docker rm ${containerName} 2>/dev/null || true`); + const existingContainer = docker.getContainer(containerName); + await existingContainer.stop(); + await existingContainer.remove(); } catch (error) { // Ignore errors when stopping/removing non-existent containers } @@ -186,34 +157,59 @@ export async function startSeiChain( // Pull the image const imageTag = `ghcr.io/sei-protocol/sei:${version}`; - await execAsync(`docker pull ${imageTag}`); + await new Promise((resolve, reject) => { + docker.pull(imageTag, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) { + reject(err); + return; + } - // Create data directory - await execAsync(`mkdir -p ${dataDir}`); + docker.modem.followProgress(stream, (err: Error | null) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }); - // Build docker run command - const dockerCmd = [ - 'docker run -d', - `--name ${containerName}`, - `-p ${rpcPort}:26657`, - `-p ${restPort}:1317`, - `-p ${grpcPort}:9090`, - `-p ${evmRpcPort}:8545`, - `-p ${p2pPort}:26656`, - `-v ${dataDir}:/root/.sei`, - imageTag, - 'seid start', - `--chain-id ${chainId}`, - `--moniker ${moniker}`, - '--rpc.laddr tcp://0.0.0.0:26657', - '--api.enable true', - '--api.address tcp://0.0.0.0:1317', - '--grpc.address 0.0.0.0:9090', - '--evm-rpc.address 0.0.0.0:8545' - ].join(' '); + // Create and start the container using dockerode + const container = await docker.createContainer({ + Image: imageTag, + name: containerName, + Cmd: [ + 'seid', 'start', + '--chain-id', chainId, + '--moniker', moniker, + '--rpc.laddr', 'tcp://0.0.0.0:26657', + '--api.enable', 'true', + '--api.address', 'tcp://0.0.0.0:1317', + '--grpc.address', '0.0.0.0:9090', + '--evm-rpc.address', '0.0.0.0:8545' + ], + ExposedPorts: { + '26657/tcp': {}, + '1317/tcp': {}, + '9090/tcp': {}, + '8545/tcp': {}, + '26656/tcp': {} + }, + HostConfig: { + PortBindings: { + '26657/tcp': [{ HostPort: rpcPort.toString() }], + '1317/tcp': [{ HostPort: restPort.toString() }], + '9090/tcp': [{ HostPort: grpcPort.toString() }], + '8545/tcp': [{ HostPort: evmRpcPort.toString() }], + '26656/tcp': [{ HostPort: p2pPort.toString() }] + }, + Binds: [`${dataDir}:/root/.sei`] + } + }); - const { stdout } = await execAsync(dockerCmd); - const containerId = stdout.trim(); + await container.start(); + const containerInfo = await container.inspect(); + const containerId = containerInfo.Id; return { containerId, @@ -236,8 +232,9 @@ export async function startSeiChain( */ export async function stopSeiChain(containerName: string): Promise { try { - await execAsync(`docker stop ${containerName}`); - await execAsync(`docker rm ${containerName}`); + const container = docker.getContainer(containerName); + await container.stop(); + await container.remove(); } catch (error) { throw new Error(`Failed to stop Sei chain: ${error instanceof Error ? error.message : String(error)}`); } @@ -248,27 +245,24 @@ export async function stopSeiChain(containerName: string): Promise { */ export async function getSeiChainStatus(): Promise { try { - const { stdout } = await execAsync( - 'docker ps -a --filter "ancestor=ghcr.io/sei-protocol/sei" --format "table {{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}\\t{{.CreatedAt}}"' + const containers = await docker.listContainers({ all: true }); + + // Filter for Sei chain containers + const seiContainers = containers.filter(container => + container.Image.includes('ghcr.io/sei-protocol/sei') || + container.Names.some(name => name.includes('sei-chain')) ); - const lines = stdout.trim().split('\n'); - if (lines.length <= 1) { - return []; - } - - // Skip header line and parse container info - return lines.slice(1).map((line) => { - const [id, name, image, status, ports, created] = line.split('\t'); - return { - id: id?.trim() || '', - name: name?.trim() || '', - image: image?.trim() || '', - status: status?.trim() || '', - ports: ports?.trim() || '', - created: created?.trim() || '' - }; - }); + return seiContainers.map(container => ({ + id: container.Id.substring(0, 12), + name: container.Names[0]?.replace('/', '') || '', + image: container.Image, + status: container.Status, + ports: container.Ports.map(port => + port.PublicPort ? `${port.PublicPort}:${port.PrivatePort}` : `${port.PrivatePort}` + ).join(', '), + created: new Date(container.Created * 1000).toISOString() + })); } catch (error) { throw new Error(`Failed to get Sei chain status: ${error instanceof Error ? error.message : String(error)}`); } @@ -279,8 +273,15 @@ export async function getSeiChainStatus(): Promise { */ export async function getSeiChainLogs(containerName: string, lines = 100): Promise { try { - const { stdout } = await execAsync(`docker logs --tail ${lines} ${containerName}`); - return stdout; + const container = docker.getContainer(containerName); + const logStream = await container.logs({ + stdout: true, + stderr: true, + tail: lines, + follow: false + }); + + return logStream.toString(); } catch (error) { throw new Error(`Failed to get Sei chain logs: ${error instanceof Error ? error.message : String(error)}`); } From 5a271de477b166ad13848f686728c6f571693a11 Mon Sep 17 00:00:00 2001 From: carson Date: Mon, 8 Sep 2025 09:47:43 -0700 Subject: [PATCH 4/5] Added changeset --- .changeset/cold-sites-hear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-sites-hear.md diff --git a/.changeset/cold-sites-hear.md b/.changeset/cold-sites-hear.md new file mode 100644 index 000000000..a93e45e0f --- /dev/null +++ b/.changeset/cold-sites-hear.md @@ -0,0 +1,5 @@ +--- +"@sei-js/mcp-server": minor +--- + +Add local docker tools to create and start local sei chains From de49cce0242e9ad2a5cb28e140b3c10e14d007b1 Mon Sep 17 00:00:00 2001 From: carson <104383295+codebycarson@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:52:10 -0700 Subject: [PATCH 5/5] Update packages/mcp-server/src/docker/releases.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/mcp-server/src/docker/releases.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/src/docker/releases.ts b/packages/mcp-server/src/docker/releases.ts index da1379071..2bc7c9264 100644 --- a/packages/mcp-server/src/docker/releases.ts +++ b/packages/mcp-server/src/docker/releases.ts @@ -33,8 +33,8 @@ export async function getSeiReleases(): Promise { // Sort by published date (newest first) return releases.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()); } catch (error) { - // Fallback to hardcoded versions if API fails - console.warn('Failed to fetch releases from GitHub API, using fallback versions:', error); + // No fallback versions are provided; no releases will be available if API fails + console.warn('Failed to fetch releases from GitHub API; no releases will be available:', error); return []; }