Add SSE transport, Docker CI/CD pipeline, and make mnemonic a per-call parameter
This commit is contained in:
@@ -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
61
.github/workflows/deploy.yml
vendored
Normal 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
7
Dockerfile
Normal 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
4
src/env.d.ts
vendored
@@ -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;
|
||||
|
||||
46
src/index.ts
46
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<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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user