diff --git a/.env.example b/.env.example index c7ee6b6..ba6753c 100644 --- a/.env.example +++ b/.env.example @@ -7,10 +7,6 @@ VECTOR_KOIOS_URL=https://koios.vector.testnet.apexfusion.org/ VECTOR_SUBMIT_URL=https://submit.vector.testnet.apexfusion.org/api/submit/tx VECTOR_EXPLORER_URL=https://vector.testnet.apexscan.org -# Wallet Configuration -VECTOR_MNEMONIC=word1 word2 word3 ... word15 # Must be exactly 15 or 24 words -VECTOR_ACCOUNT_INDEX=0 # Wallet account index (default: 0) - # Safety Limits (in lovelace, 1 ADA = 1,000,000 lovelace) VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA per transaction VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA daily limit diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..9a9083d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,61 @@ +name: Build and Deploy + +on: + push: + branches: [main] + +jobs: + build-and-deploy: + runs-on: self-hosted + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + - name: Build TypeScript + run: npm run build + env: + NODE_OPTIONS: --max-old-space-size=12288 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} + + - name: Deploy to Hetzner VM + run: | + echo "${{ secrets.HETZNER_SSH_KEY }}" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no \ + ${{ secrets.HETZNER_USER }}@${{ secrets.HETZNER_HOST }} \ + "echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin && \ + docker pull ghcr.io/${{ github.repository }}:${{ github.sha }} && \ + docker stop vector-mcp || true && \ + docker rm vector-mcp || true && \ + docker run -d \ + --name vector-mcp \ + --restart unless-stopped \ + -p 3000:3000 \ + --env-file /etc/vector-mcp/env \ + ghcr.io/${{ github.repository }}:${{ github.sha }}" + rm /tmp/deploy_key diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9aabeb3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY build/ ./build/ +EXPOSE 3000 +CMD ["node", "build/index.js"] diff --git a/src/env.d.ts b/src/env.d.ts index 1da9f4d..45298a4 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -6,10 +6,6 @@ declare namespace NodeJS { VECTOR_SUBMIT_URL: string; VECTOR_EXPLORER_URL: string; - // Wallet Configuration - VECTOR_MNEMONIC: string; - VECTOR_ACCOUNT_INDEX: string; - // Safety Limits VECTOR_SPEND_LIMIT_PER_TX: string; VECTOR_SPEND_LIMIT_DAILY: string; diff --git a/src/index.ts b/src/index.ts index 58d1cf7..2a41cb8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { config } from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import { existsSync } from 'fs'; +import { createServer } from 'http'; // Get directory name for ES module const __filename = fileURLToPath(import.meta.url); @@ -23,7 +24,7 @@ if (existsSync(envPath)) { } import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { registerVectorTools } from "./vector/index.js"; // Create server instance @@ -36,13 +37,42 @@ const server = new McpServer({ console.error('Registering Vector tools...'); registerVectorTools(server); -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); -} +const PORT = parseInt(process.env.PORT || '3000'); +const transports = new Map(); -main().catch((err: unknown) => { - const error = err as Error; - console.error("Fatal error in main():", error.message); +const httpServer = createServer(async (req, res) => { + const url = new URL(req.url!, `http://localhost:${PORT}`); + + if (req.method === 'GET' && url.pathname === '/sse') { + const transport = new SSEServerTransport('/messages', res); + transports.set(transport.sessionId, transport); + res.on('close', () => transports.delete(transport.sessionId)); + await server.connect(transport); + } else if (req.method === 'POST' && url.pathname === '/messages') { + const sessionId = url.searchParams.get('sessionId'); + if (!sessionId) { + res.writeHead(400); + res.end('Missing sessionId'); + return; + } + const transport = transports.get(sessionId); + if (!transport) { + res.writeHead(404); + res.end('Session not found'); + return; + } + await transport.handlePostMessage(req, res); + } else { + res.writeHead(404); + res.end('Not found'); + } +}); + +httpServer.listen(PORT, () => { + console.error(`Vector MCP Server listening on port ${PORT}`); +}); + +httpServer.on('error', (err: Error) => { + console.error('Fatal error in HTTP server:', err.message); process.exit(1); }); diff --git a/src/vector/agent-network.ts b/src/vector/agent-network.ts index afe0fcd..29edccc 100644 --- a/src/vector/agent-network.ts +++ b/src/vector/agent-network.ts @@ -13,8 +13,6 @@ const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vecto const VECTOR_SUBMIT_URL = process.env.VECTOR_SUBMIT_URL || 'https://submit.vector.testnet.apexfusion.org/api/submit/tx'; const VECTOR_KOIOS_URL = process.env.VECTOR_KOIOS_URL || 'https://koios.vector.testnet.apexfusion.org/'; const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org'; -const VECTOR_MNEMONIC = process.env.VECTOR_MNEMONIC || ''; -const VECTOR_ACCOUNT_INDEX = parseInt(process.env.VECTOR_ACCOUNT_INDEX || '0'); // Registry constants const REGISTRY_SCRIPT_CBOR = "59058101010029800aba2aba1aba0aab9faab9eaab9dab9a48888889660033001300337540112232330010010032259800800c52845660026006601400314a3133002002300b001401480424600e60106010003223232330010010042259800800c00e2646644b30013372200e00515980099b8f0070028800c01900944cc014014c03c0110091bae3008001375660120026016002804852f5bded8c1230073008300830083008001918039804000cdc3a4005370e90002444444453001300f0089807004496600266e1d2004300b3754600e60186ea80062946294100a4888c8cc88cc008008004896600200300389919912cc004cdc8803801456600266e3c01c00a20030064049133005005301800440486eb8c044004dd69809000980a000a02433009004003148001222232980098079baa00194c004006910100a44100400d301300548896600260120071323322598009806000c4c9660026036003132598009807180b9baa0018991919912cc004c08000e0111640746eb4c074004dd7180e801180e800980c1baa0018b202c301a0018b2030301637540091598009806800c566002602c6ea801200516405d16405080a0566002601460266ea800a2646644b30013301337586034602e6ea802c8cdd7980d980c1baa0010048acc004cc89660020030028acc004c074006264b30013371e6eb8c064004016260226eb4c0680062941018180e000c00901a203414a0660226eacc040c05cdd500580244cc04cdd61809180b9baa00b25980099baf301b301837540020051598009805800c566002601f30013756602060306ea800600b003402913371290406d620498039bab30103018375400314a080b229410164528202c8a50405514a080a8dca1bb30013374a90001980b99ba548008cc05cdd480125eb80cc05d300103d87a80004bd70180b980a1baa002899912cc004006005159800980d000c4cdc39bad301630190014800600480b90170a503300e3756601a60286ea802000501218099baa002375c602c60266ea80122b3001300a003899199119912cc004c0380062b30013018375400d0028b20328acc004c03c0062b30013018375400d0028b20328b202c40582b3001300c3015375400319800980c980b1baa001912cc004c038c05cdd5000c4c8c8cc004004dd6180e980f180f180f180f180f180f180f180f180d1baa0042259800800c528456600266e3cdd7180f000801c528c4cc008008c07c0050192038375c603660306ea80062941016488c8cc00400400c896600200314c0103d87a80008992cc004c010006266e9520003301d0014bd7044cc00c00cc07c009019180e800a0369192cc004c03cc05cdd5000c4dd7180d980c1baa0018b202c301a301737540029111192cc004c044c068dd5004c4c8c8c9660026600e0246042603c6ea80222660346eb0c064c078dd500912cc004cdd79811180f9baa0010048acc004c0480062b3001301698009bab3017301f37540030038012022899b89482036b1024c038dd5980b980f9baa0018a50407514a080ea294101d4528203832598009811000c4c966002602c6eb4c07c00626eb8c0780062c80e8c0840062c80f8cc05cc9660026028603a6ea800626eacc058c078dd5180b180f1baa3021301e37540031640706600a6eb0c080c074dd5008919baf3021301e37540020140026006002660026eb0c078c06cdd5007803c5660026600801e603c60366ea801626644b30010018014566002604200313370e6eb4c074c0800052001801203c40782940cc054dd5980a180d9baa00f3002330013758603c60366ea803c01e294101920322232598009809980e1baa00189810180e9baa3015301d37546040603a6ea80062c80d8cc0100088cdd79810180e9baa001002459014180a9baa00430170013017301800130133754009164044808860246026008452689b2b200201"; @@ -76,20 +74,20 @@ function parseAgentDatum(datumCbor, utxoRef, assets) { } } -async function initLucid() { +async function initLucid(mnemonic: string, accountIndex: number = 0) { const provider = new OgmiosProvider({ ogmiosUrl: VECTOR_OGMIOS_URL, submitUrl: VECTOR_SUBMIT_URL, koiosUrl: VECTOR_KOIOS_URL, }); const lucid = await Lucid.new(provider, 'Mainnet'); - if (!VECTOR_MNEMONIC) throw new Error('VECTOR_MNEMONIC is required'); - const trimmed = VECTOR_MNEMONIC.trim(); + if (!mnemonic) throw new Error('mnemonic is required'); + const trimmed = mnemonic.trim(); const words = trimmed.split(/\s+/); if (words.length !== 15 && words.length !== 24) { throw new Error(`Invalid mnemonic: Expected 15 or 24 words, got ${words.length}`); } - lucid.selectWalletFromSeed(trimmed, { accountIndex: VECTOR_ACCOUNT_INDEX }); + lucid.selectWalletFromSeed(trimmed, { accountIndex }); return lucid; } @@ -161,17 +159,18 @@ ${capList} "vector_discover_agents", "Discover registered agents in the Vector on-chain registry, optionally filtered by capability or framework", { + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic (used to derive the registry address)"), capability: z.string().optional().describe("Filter by capability tag (e.g. 'investing', 'research')"), framework: z.string().optional().describe("Filter by framework (e.g. 'OpenClaw', 'LangChain', 'CrewAI')"), limit: z.number().optional().default(20).describe("Maximum number of agents to return (default: 20)"), }, - async ({ capability, framework, limit }) => { + async ({ mnemonic, capability, framework, limit }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const registryScript = { type: "PlutusV3", script: REGISTRY_SCRIPT_CBOR }; const registryAddress = lucid.utils.validatorToAddress(registryScript); const provider = new OgmiosProvider({ ogmiosUrl: VECTOR_OGMIOS_URL, submitUrl: VECTOR_SUBMIT_URL, koiosUrl: VECTOR_KOIOS_URL }); @@ -218,15 +217,16 @@ ${capList} // vector_register_agent server.tool( "vector_register_agent", - "Register this agent in the Vector on-chain agent registry. Mints a soulbound identity NFT and locks a 10 AP3X deposit.", + "Register an agent in the Vector on-chain agent registry. Mints a soulbound identity NFT and locks a 10 AP3X deposit.", { + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the registering wallet"), name: z.string().min(1).max(64).describe("Agent name (e.g. 'TradingBot', 'ResearchAgent')"), description: z.string().max(256).describe("Short description of the agent's purpose"), capabilities: z.array(z.string()).describe("List of capability tags (e.g. ['investing', 'research', 'environmental'])"), framework: z.string().describe("Framework used (e.g. 'OpenClaw', 'LangChain', 'CrewAI', 'custom')"), endpoint: z.string().describe("A2A communication endpoint URL (or empty string if not applicable)"), }, - async ({ name, description, capabilities, framework, endpoint }) => { + async ({ mnemonic, name, description, capabilities, framework, endpoint }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -235,7 +235,7 @@ ${capList} const safetyCheck = safetyLayer.checkTransaction(Number(MIN_AP3X_DEPOSIT)); if (!safetyCheck.allowed) throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const walletAddress = await lucid.wallet.address(); const addressDetails = lucid.utils.getAddressDetails(walletAddress); const vkeyHash = addressDetails.paymentCredential?.hash; @@ -308,8 +308,9 @@ Save your Agent DID — you'll need it to update, deregister, or let other agent agent_id: z.string().describe("Recipient agent DID: did:vector:agent:{policyId}:{nftAssetName}"), message_type: z.enum(["inquiry", "proposal", "result"]).describe("Type of message"), payload: z.string().max(512).describe("Message payload (string, max 512 chars)"), + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the sending wallet"), }, - async ({ agent_id, message_type, payload }) => { + async ({ agent_id, message_type, payload, mnemonic }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -326,7 +327,7 @@ Save your Agent DID — you'll need it to update, deregister, or let other agent const profile = parseAgentDatum(utxo.datum, `${utxo.txHash}#${utxo.outputIndex}`, utxo.assets); if (!profile?.ownerVkeyHash) throw new Error('Could not parse agent owner from registry datum'); - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const senderAddress = await lucid.wallet.address(); const recipientAddress = lucid.utils.credentialToAddress({ type: 'Key', hash: profile.ownerVkeyHash }); const minAda = 2_000_000n; diff --git a/src/vector/vector.ts b/src/vector/vector.ts index d29fe16..cb6c7f8 100644 --- a/src/vector/vector.ts +++ b/src/vector/vector.ts @@ -39,8 +39,6 @@ const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vecto const VECTOR_SUBMIT_URL = process.env.VECTOR_SUBMIT_URL || 'https://submit.vector.testnet.apexfusion.org/api/submit/tx'; const VECTOR_KOIOS_URL = process.env.VECTOR_KOIOS_URL || 'https://koios.vector.testnet.apexfusion.org/'; const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org'; -const VECTOR_MNEMONIC = process.env.VECTOR_MNEMONIC || ''; -const VECTOR_ACCOUNT_INDEX = parseInt(process.env.VECTOR_ACCOUNT_INDEX || '0'); // Helper function to format ADA amounts function lovelaceToAda(lovelace: string | number | bigint): string { @@ -65,7 +63,7 @@ function explorerTxLink(txHash: string): string { } // Initialize Lucid instance with Ogmios provider -async function initLucid() { +async function initLucid(mnemonic: string, accountIndex: number = 0) { const provider = new OgmiosProvider({ ogmiosUrl: VECTOR_OGMIOS_URL, submitUrl: VECTOR_SUBMIT_URL, @@ -75,18 +73,18 @@ async function initLucid() { // Vector uses --mainnet flag, so addresses are addr1... format const lucid = await Lucid.new(provider, 'Mainnet'); - if (!VECTOR_MNEMONIC) { - throw new Error('VECTOR_MNEMONIC is required in .env file'); + if (!mnemonic) { + throw new Error('mnemonic is required'); } - const trimmedMnemonic = VECTOR_MNEMONIC.trim(); + const trimmedMnemonic = mnemonic.trim(); const words = trimmedMnemonic.split(/\s+/); if (words.length !== 15 && words.length !== 24) { throw new Error(`Invalid mnemonic: Expected 15 or 24 words, got ${words.length}`); } - lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex: VECTOR_ACCOUNT_INDEX }); + lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex }); const address = await lucid.wallet.address(); if (!address) { @@ -97,8 +95,8 @@ async function initLucid() { } // Get wallet info -export async function getWalletInfo(): Promise { - const lucid = await initLucid(); +export async function getWalletInfo(mnemonic: string, accountIndex: number = 0): Promise { + const lucid = await initLucid(mnemonic, accountIndex); const address = await lucid.wallet.address(); const utxos = await lucid.utxosAt(address); @@ -153,6 +151,7 @@ export async function getWalletInfo(): Promise { export async function sendAda( recipientAddress: string, amountAda: number, + mnemonic: string, metadata: any = null ): Promise { if (!recipientAddress) { @@ -171,7 +170,7 @@ export async function sendAda( throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); } - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const senderAddress = await lucid.wallet.address(); // Validate recipient address @@ -219,6 +218,7 @@ export async function sendTokens( policyId: string, assetName: string, amount: string, + mnemonic: string, adaAmount: number | null = null ): Promise { if (!recipientAddress) throw new Error('Recipient address is required'); @@ -234,7 +234,7 @@ export async function sendTokens( throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); } - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const senderAddress = await lucid.wallet.address(); try { @@ -292,6 +292,7 @@ export async function sendTokens( // Build a complex multi-output transaction export async function buildTransaction( outputs: TxOutput[], + mnemonic: string, metadata: any = null, submit: boolean = false, ): Promise { @@ -306,7 +307,7 @@ export async function buildTransaction( throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); } - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); // @ts-ignore let tx = lucid.newTx(); @@ -372,6 +373,7 @@ export async function buildTransaction( export async function deployContract( scriptCbor: string, scriptType: string, + mnemonic: string, initialDatum: string | null = null, lovelaceAmount: number = 2_000_000, ): Promise { @@ -380,7 +382,7 @@ export async function deployContract( throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); } - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const validator: SpendingValidator = { type: scriptType as any, @@ -420,13 +422,14 @@ export async function interactWithContract( scriptCbor: string, scriptType: string, action: 'spend' | 'lock', + mnemonic: string, redeemer: string | null = null, datum: string | null = null, lovelaceAmount: number = 2_000_000, utxoRef: { txHash: string; outputIndex: number } | null = null, assets: Record | null = null, ): Promise { - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); const walletAddress = await lucid.wallet.address(); const validator: SpendingValidator = { @@ -599,15 +602,17 @@ ${tokens.length > 0 ? `Token Holdings (${tokens.length}):\n${tokenList}` : 'No t // @ts-ignore: MCP SDK deep type instantiation server.tool( "vector_get_address", - "Get the agent's Vector wallet address, balance, and token holdings", - {}, - async () => { + "Get the Vector wallet address, balance, and token holdings derived from a mnemonic", + { + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the wallet"), + }, + async ({ mnemonic }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - const walletInfo = await getWalletInfo(); + const walletInfo = await getWalletInfo(mnemonic); const tokenList = walletInfo.tokens.length > 0 ? walletInfo.tokens.map((t: VectorToken) => `${t.quantity} ${t.name}`).join('\n') @@ -633,7 +638,7 @@ ${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length} text: `Failed to get wallet information: ${error.message} **Troubleshooting Tips:** -1. Make sure you have a valid 15 or 24-word mnemonic in VECTOR_MNEMONIC +1. Make sure you have a valid 15 or 24-word BIP39 mnemonic 2. Verify the Ogmios endpoint is reachable: ${VECTOR_OGMIOS_URL} 3. Check the console logs for detailed error information`, }], @@ -646,11 +651,12 @@ ${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length} // @ts-ignore: MCP SDK deep type instantiation server.tool( "vector_get_utxos", - "List unspent transaction outputs (UTxOs) for a Vector address or the agent's wallet", + "List unspent transaction outputs (UTxOs) for a Vector address or a wallet derived from a mnemonic", { - address: z.string().optional().describe("Vector address to query UTxOs for. If omitted, uses the agent's wallet."), + address: z.string().optional().describe("Vector address to query UTxOs for. If omitted, mnemonic is required."), + mnemonic: z.string().optional().describe("15 or 24-word BIP39 mnemonic (required if address is omitted)"), }, - async ({ address }) => { + async ({ address, mnemonic }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -668,7 +674,8 @@ ${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length} utxos = await provider.getUtxos(address); queryAddress = address; } else { - const lucid = await initLucid(); + if (!mnemonic) throw new Error('Provide either address or mnemonic'); + const lucid = await initLucid(mnemonic); queryAddress = await lucid.wallet.address(); utxos = await lucid.utxosAt(queryAddress); } @@ -714,14 +721,15 @@ ${utxoList}`, // @ts-ignore: MCP SDK deep type instantiation server.tool( "vector_send_apex", - "Send ADA from the agent's wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting (transaction-crafter mode).", + "Send ADA from a wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting (transaction-crafter mode).", { recipientAddress: z.string().describe("Recipient Vector address (addr1...)"), amount: z.number().min(1).describe("Amount of ADA to send"), + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the sending wallet"), metadata: z.string().optional().describe("Optional transaction metadata in JSON format"), unsigned_only: z.boolean().optional().default(false).describe("If true, return unsigned TX CBOR without signing or submitting"), }, - async ({ recipientAddress, amount, metadata, unsigned_only }) => { + async ({ recipientAddress, amount, mnemonic, metadata, unsigned_only }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -729,7 +737,7 @@ ${utxoList}`, try { if (unsigned_only) { const lovelaceAmount = BigInt(Math.floor(amount * 1_000_000)); - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); // @ts-ignore let txBuilder = lucid.newTx().payToAddress(recipientAddress, { lovelace: lovelaceAmount }); if (metadata) { @@ -762,7 +770,7 @@ This transaction has NOT been submitted. Sign and submit it separately.`, }; } - const result = await sendAda(recipientAddress, amount, metadata); + const result = await sendAda(recipientAddress, amount, mnemonic, metadata); return { content: [{ type: "text", @@ -798,23 +806,24 @@ Amount: ${result.amount} ADA // @ts-ignore: MCP SDK deep type instantiation server.tool( "vector_send_tokens", - "Send Vector native tokens from the agent's wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting.", + "Send Vector native tokens from a wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting.", { recipientAddress: z.string().describe("Recipient Vector address (addr1...)"), policyId: z.string().describe("Token policy ID"), assetName: z.string().describe("Asset name (can be empty for policy-only tokens)"), amount: z.string().describe("Amount of tokens to send"), + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the sending wallet"), adaAmount: z.number().optional().describe("Optional ADA to include (uses minimum required if not specified)"), unsigned_only: z.boolean().optional().default(false).describe("If true, return unsigned TX CBOR without signing or submitting"), }, - async ({ recipientAddress, policyId, assetName, amount, adaAmount, unsigned_only }) => { + async ({ recipientAddress, policyId, assetName, amount, mnemonic, adaAmount, unsigned_only }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { if (unsigned_only) { - const lucid = await initLucid(); + const lucid = await initLucid(mnemonic); let assetNameHex = assetName; if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) { assetNameHex = fromText(assetName); @@ -852,7 +861,7 @@ This transaction has NOT been submitted. Sign and submit it separately.`, }; } - const result = await sendTokens(recipientAddress, policyId, assetName, amount, adaAmount); + const result = await sendTokens(recipientAddress, policyId, assetName, amount, mnemonic, adaAmount); return { content: [{ type: "text", @@ -946,16 +955,17 @@ ${log.length > 0 ? `## Recent Transactions (last ${Math.min(5, log.length)}):\n$ lovelace: z.number().describe("Amount in lovelace (1 ADA = 1,000,000 lovelace)"), assets: z.record(z.string()).optional().describe("Optional native assets: { 'policyId+assetNameHex': 'quantity' }"), })).min(1).describe("Transaction outputs"), + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the signing wallet"), metadata: z.string().optional().describe("Optional JSON metadata (attached under label 674)"), submit: z.boolean().optional().describe("If true, sign and submit the transaction. If false/omitted, return unsigned CBOR for review."), }, - async ({ outputs, metadata, submit }) => { + async ({ outputs, mnemonic, metadata, submit }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - const result = await buildTransaction(outputs, metadata, submit); + const result = await buildTransaction(outputs, mnemonic, metadata, submit); if (result.submitted) { return { @@ -1017,9 +1027,10 @@ Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction lovelace: z.number().describe("Amount in lovelace"), assets: z.record(z.string()).optional(), })).optional().describe("If no txCbor provided, build a TX from these outputs and evaluate it"), + mnemonic: z.string().optional().describe("15 or 24-word BIP39 mnemonic (required when outputs is provided)"), metadata: z.string().optional().describe("Optional JSON metadata when building from outputs"), }, - async ({ txCbor, outputs, metadata }) => { + async ({ txCbor, outputs, mnemonic, metadata }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -1030,7 +1041,8 @@ Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction if (!cborToEvaluate && outputs && outputs.length > 0) { // Build the transaction first - const lucid = await initLucid(); + if (!mnemonic) throw new Error('mnemonic is required when building from outputs'); + const lucid = await initLucid(mnemonic); // @ts-ignore let tx = lucid.newTx(); @@ -1142,11 +1154,12 @@ No transaction was submitted to the network.`, "vector_get_transaction_history", "Get transaction history for a Vector address via Koios indexed queries", { - address: z.string().optional().describe("Vector address to query. If omitted, uses the agent's wallet."), + address: z.string().optional().describe("Vector address to query. If omitted, mnemonic is required."), + mnemonic: z.string().optional().describe("15 or 24-word BIP39 mnemonic (required if address is omitted)"), limit: z.number().min(1).max(50).optional().describe("Number of transactions to return (default: 20, max: 50)"), offset: z.number().min(0).optional().describe("Offset for pagination (default: 0)"), }, - async ({ address, limit, offset }) => { + async ({ address, mnemonic, limit, offset }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -1154,7 +1167,8 @@ No transaction was submitted to the network.`, try { let queryAddress = address; if (!queryAddress) { - const lucid = await initLucid(); + if (!mnemonic) throw new Error('Provide either address or mnemonic'); + const lucid = await initLucid(mnemonic); queryAddress = await lucid.wallet.address(); } @@ -1217,10 +1231,11 @@ ${txList} { scriptCbor: z.string().describe("Compiled Plutus/Aiken script in CBOR hex format"), scriptType: z.enum(["PlutusV1", "PlutusV2", "PlutusV3"]).describe("Script version"), + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the deploying wallet"), initialDatum: z.string().optional().describe("Initial datum as CBOR hex. Use 'd87980' for void/unit datum. Defaults to void if omitted."), lovelaceAmount: z.number().optional().describe("ADA to lock at the script address in lovelace (default: 2,000,000 = 2 ADA)"), }, - async ({ scriptCbor, scriptType, initialDatum, lovelaceAmount }) => { + async ({ scriptCbor, scriptType, mnemonic, initialDatum, lovelaceAmount }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -1229,6 +1244,7 @@ ${txList} const result = await deployContract( scriptCbor, scriptType, + mnemonic, initialDatum || null, lovelaceAmount || 2_000_000, ); @@ -1275,6 +1291,7 @@ Funds locked at the script address. Use vector_interact_contract to interact wit scriptCbor: z.string().describe("Compiled Plutus/Aiken script in CBOR hex"), scriptType: z.enum(["PlutusV1", "PlutusV2", "PlutusV3"]).describe("Script version"), action: z.enum(["spend", "lock"]).describe("'spend' to collect UTxOs from the script, 'lock' to send funds to it"), + mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the wallet"), redeemer: z.string().optional().describe("Redeemer as CBOR hex (required for spend, use 'd87980' for void)"), datum: z.string().optional().describe("Datum as CBOR hex (required for lock, use 'd87980' for void)"), lovelaceAmount: z.number().optional().describe("Lovelace to lock (for lock action, default: 2,000,000 = 2 ADA)"), @@ -1284,7 +1301,7 @@ Funds locked at the script address. Use vector_interact_contract to interact wit }).optional().describe("Specific UTxO to spend from (optional, otherwise spends all UTxOs at script address)"), assets: z.record(z.string()).optional().describe("Additional native assets for lock action: { 'policyId+assetNameHex': 'quantity' }"), }, - async ({ scriptCbor, scriptType, action, redeemer, datum, lovelaceAmount, utxoRef, assets }) => { + async ({ scriptCbor, scriptType, action, mnemonic, redeemer, datum, lovelaceAmount, utxoRef, assets }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; @@ -1294,6 +1311,7 @@ Funds locked at the script address. Use vector_interact_contract to interact wit scriptCbor, scriptType, action, + mnemonic, redeemer || null, datum || null, lovelaceAmount || 2_000_000,