Add SSE transport, Docker CI/CD pipeline, and make mnemonic a per-call parameter

This commit is contained in:
david
2026-03-18 23:56:24 +01:00
parent 2ae246dd40
commit e1528d575e
7 changed files with 178 additions and 69 deletions

View File

@@ -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

61
.github/workflows/deploy.yml vendored Normal file
View File

@@ -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

7
Dockerfile Normal file
View File

@@ -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"]

4
src/env.d.ts vendored
View File

@@ -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;

View File

@@ -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<string, SSEServerTransport>();
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);
});

View File

@@ -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;

View File

@@ -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<VectorWalletInfo> {
const lucid = await initLucid();
export async function getWalletInfo(mnemonic: string, accountIndex: number = 0): Promise<VectorWalletInfo> {
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<VectorWalletInfo> {
export async function sendAda(
recipientAddress: string,
amountAda: number,
mnemonic: string,
metadata: any = null
): Promise<VectorAdaTransactionResult> {
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<VectorTokenTransactionResult> {
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<VectorBuildTransactionResult> {
@@ -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<VectorDeployContractResult> {
@@ -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<string, string> | null = null,
): Promise<VectorInteractContractResult> {
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,