Merge pull request #6 from FilipBl4gojevic/refactor/workspaces-shared-builder

Split into npm workspaces: shared + builder
This commit is contained in:
dcejtlin-apex
2026-07-28 10:39:47 +02:00
committed by GitHub
27 changed files with 581 additions and 1744 deletions

View File

@@ -1,15 +1,18 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY packages/shared/package.json ./packages/shared/
COPY packages/builder/package.json ./packages/builder/
RUN npm ci
COPY tsup.config* tsconfig* ./
COPY src/ ./src/
COPY packages/ ./packages/
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
COPY packages/shared/package.json ./packages/shared/
COPY packages/builder/package.json ./packages/builder/
RUN npm ci --omit=dev
COPY --from=builder /app/build/ ./build/
COPY --from=builder /app/packages/builder/build/ ./packages/builder/build/
EXPOSE 3000
CMD ["node", "build/index.js"]
CMD ["node", "packages/builder/build/index.js"]

View File

@@ -128,7 +128,7 @@ Add to your Claude Desktop MCP config (`claude_desktop_config.json`):
"mcpServers": {
"vector": {
"command": "node",
"args": ["/path/to/vector-mcp-server/build/index.js"],
"args": ["/path/to/vector-mcp-server/packages/builder/build/index.js"],
"env": {
"VECTOR_OGMIOS_URL": "https://ogmios.vector.testnet.apexfusion.org",
"VECTOR_SUBMIT_URL": "https://submit.vector.testnet.apexfusion.org/api/submit/tx",
@@ -188,11 +188,11 @@ npm run test:smoke
Builds the server, boots it, and asserts the exposed tool inventory matches the checked-in snapshot. No wallet, no external network. CI runs this and `test:unit` on every PR.
```bash
echo "your mnemonic words here" > mnemonic.txt
echo "your mnemonic words here" > packages/builder/mnemonic.txt
npm run test:integration
```
Requires `mnemonic.txt` in the repo root containing a **funded Vector testnet** mnemonic. Covers the core tools end-to-end against Vector testnet, including the full agent lifecycle: register, discover, profile, update, transfer, message, and deregister. Never runs in CI.
Requires `mnemonic.txt` in `packages/builder/` containing a **funded Vector testnet** mnemonic. Covers the core tools end-to-end against Vector testnet, including the full agent lifecycle: register, discover, profile, update, transfer, message, and deregister. Never runs in CI.
## Architecture

1844
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,36 @@
{
"name": "@apexfusion/mcp-server",
"name": "@apexfusion/mcp-server-monorepo",
"version": "0.1.0",
"main": "index.js",
"private": true,
"type": "module",
"bin": {
"mcp-server": "./build/index.js"
},
"workspaces": ["packages/*"],
"scripts": {
"postinstall": "ln -sf ../../../libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs node_modules/libsodium-wrappers-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs 2>/dev/null || true",
"clean": "rm -rf build",
"build": "tsup && chmod +x build/index.js",
"typecheck": "tsc --noEmit --incremental",
"start": "node build/index.js",
"clean": "rm -rf packages/*/build",
"build": "npm run build --workspace=@apexfusion/vector-mcp-builder",
"typecheck": "tsc --noEmit -p packages/shared && tsc --noEmit -p packages/builder",
"start": "node packages/builder/build/index.js",
"test": "npm run test:unit",
"test:unit": "node --import tsx --test test/unit/*.test.ts",
"test:integration": "npm run build && node --import tsx --test test/integration/*.test.ts",
"test:smoke": "npm run build && node --import tsx --test test/smoke/*.test.ts"
},
"files": [
"build"
],
"keywords": [
"vector",
"mcp",
"blockchain",
"cardano",
"apex-fusion",
"ai-agent",
"utxo"
],
"author": "Apex Fusion",
"license": "ISC",
"description": "MCP server for Vector - AI agent interface for the Apex Fusion eUTXO L2",
"repository": {
"type": "git",
"url": "https://github.com/Apex-Fusion/mcp-server.git"
},
"homepage": "https://apex-fusion.github.io/vector-ai-documentation/",
"bugs": {
"url": "https://github.com/Apex-Fusion/mcp-server/issues"
},
"engines": {
"node": ">=14.0.0"
},
"private": false,
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1027.0",
"@lucid-evolution/lucid": "^0.4.29",
"@modelcontextprotocol/sdk": "^1.4.1",
"@noble/hashes": "^1.7.1",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"zod": "^3.24.1"
"test:unit": "node --import tsx --test packages/*/test/*.test.ts",
"test:smoke": "npm run build && node --import tsx --test packages/builder/test/smoke/*.test.ts",
"test:integration": "npm run build && node --import tsx --test packages/builder/test/integration/*.test.ts"
},
"devDependencies": {
"@types/node": "^22.13.1",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=22.0.0"
},
"license": "ISC",
"author": "Apex Fusion",
"repository": {
"type": "git",
"url": "https://github.com/Apex-Fusion/mcp-server.git"
},
"bugs": {
"url": "https://github.com/Apex-Fusion/mcp-server/issues"
}
}

View File

@@ -0,0 +1,29 @@
{
"name": "@apexfusion/vector-mcp-builder",
"version": "0.1.0",
"private": false,
"type": "module",
"description": "MCP server for Vector - AI agent interface for the Apex Fusion eUTXO L2",
"bin": {
"vector-mcp-builder": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsup && chmod +x build/index.js",
"start": "node build/index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1027.0",
"@lucid-evolution/lucid": "^0.4.29",
"@modelcontextprotocol/sdk": "^1.4.1",
"@noble/hashes": "^1.7.1",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"zod": "^3.24.1"
},
"devDependencies": {
"@apexfusion/vector-mcp-shared": "*"
}
}

View File

@@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Check for .env file
const envPath = resolve(__dirname, '../.env');
const envPath = resolve(__dirname, '../../../.env');
console.error('Looking for .env file at:', envPath);
console.error('.env file exists:', existsSync(envPath));

File diff suppressed because one or more lines are too long

View File

@@ -11,4 +11,4 @@ export type {
VectorTransactionHistoryResult,
VectorDeployContractResult,
VectorInteractContractResult,
} from './types.js';
} from '@apexfusion/vector-mcp-shared/types';

View File

@@ -1,14 +1,17 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import type { SpendLimits, SpendStatus, AuditEntry } from './types.js';
import type { SpendLimits, SpendStatus, AuditEntry } from '@apexfusion/vector-mcp-shared/types';
const VECTOR_SPEND_LIMIT_PER_TX = parseInt(process.env.VECTOR_SPEND_LIMIT_PER_TX || '100000000'); // 100 AP3X
const VECTOR_SPEND_LIMIT_DAILY = parseInt(process.env.VECTOR_SPEND_LIMIT_DAILY || '500000000'); // 500 AP3X
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = resolve(__dirname, '../..');
// From the bundled entrypoint's directory (packages/builder/build), three levels
// up lands on the repo root, which is where .env.example and the README document
// the default audit log path (./vector-audit-log.json) as living.
const projectRoot = resolve(__dirname, '../../..');
export class SafetyLayer {
private dailySpent: number = 0;

View File

@@ -18,16 +18,13 @@ async function ensureSlotConfig(provider: OgmiosProvider): Promise<number> {
}
import { blake2b } from '@noble/hashes/blake2b';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { OgmiosProvider } from './ogmios-provider.js';
import { OgmiosProvider } from '@apexfusion/vector-mcp-shared/provider';
import {
VECTOR_OGMIOS_URL, VECTOR_SUBMIT_URL, VECTOR_KOIOS_URL, VECTOR_EXPLORER_URL, explorerTxLink,
} from '@apexfusion/vector-mcp-shared/config';
import { safetyLayer } from './safety.js';
import { rateLimiter } from './rate-limiter.js';
// Env config
const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vector.testnet.apexfusion.org';
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://v2.koios.vector.testnet.apexfusion.org/';
const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org';
// Agent Registry policy ID (shared across all modules)
const AGENT_REGISTRY_POLICY = process.env.AGENT_REGISTRY_POLICY || 'be1a0a2912da180757ed3cd61b56bb8eab0188c19dc3c0e3912d2c01';
@@ -84,10 +81,6 @@ function newProvider() {
return new OgmiosProvider({ ogmiosUrl: VECTOR_OGMIOS_URL, submitUrl: VECTOR_SUBMIT_URL, koiosUrl: VECTOR_KOIOS_URL });
}
function explorerTxLink(txHash: string) {
return `${VECTOR_EXPLORER_URL}/transaction/${txHash}`;
}
function lovelaceToApex(lovelace: number | bigint): string {
return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6);
}

View File

@@ -7,25 +7,25 @@ import * as dotenv from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import { OgmiosProvider } from './ogmios-provider.js';
import { OgmiosProvider } from '@apexfusion/vector-mcp-shared/provider';
import { lovelaceToAda, formatAssetName } from '@apexfusion/vector-mcp-shared/tx';
import {
VECTOR_OGMIOS_URL, VECTOR_SUBMIT_URL, VECTOR_KOIOS_URL, VECTOR_EXPLORER_URL, explorerTxLink,
} from '@apexfusion/vector-mcp-shared/config';
import { safetyLayer } from './safety.js';
import { rateLimiter } from './rate-limiter.js';
import { registerAgentNetworkTools } from './agent-network.js';
import { registerSelfImprovementTools } from './self-improvement.js';
import type {
VectorToken,
VectorWalletInfo,
VectorAdaTransactionResult,
VectorTokenTransactionResult,
TxOutput,
VectorBuildTransactionResult,
VectorDryRunResult,
VectorDeployContractResult,
VectorToken, VectorWalletInfo, VectorAdaTransactionResult, VectorTokenTransactionResult,
TxOutput, VectorBuildTransactionResult, VectorDryRunResult, VectorDeployContractResult,
VectorInteractContractResult,
} from './types.js';
} from '@apexfusion/vector-mcp-shared/types';
// Direct .env loading
const __filename = fileURLToPath(import.meta.url);
// From the bundled entrypoint at packages/builder/build/index.js, four levels
// up from the file lands on the repo root (build -> builder -> packages -> root).
const projectRoot = resolve(__filename, '../../../..');
const envPath = resolve(projectRoot, '.env');
@@ -36,39 +36,6 @@ if (existsSync(envPath)) {
}
}
// Configuration from environment variables
const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vector.testnet.apexfusion.org';
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://v2.koios.vector.testnet.apexfusion.org/';
const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org';
// Helper function to format AP3X amounts
function lovelaceToAda(lovelace: string | number | bigint): string {
if (lovelace === undefined || lovelace === null) return '0.000000';
try {
return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6);
} catch {
return '0.000000';
}
}
// Helper function to format asset name
function formatAssetName(name: string): string {
try {
if (/^[0-9a-fA-F]+$/.test(name) && name.length > 0) {
return Buffer.from(name, 'hex').toString('utf8');
}
return name;
} catch {
return name;
}
}
// Explorer link helper
function explorerTxLink(txHash: string): string {
return `${VECTOR_EXPLORER_URL}/transaction/${txHash}`;
}
// Initialize Lucid instance with Ogmios provider
async function initLucid(mnemonic: string, accountIndex: number = 0) {
const provider = new OgmiosProvider({

View File

@@ -1,19 +1,17 @@
{
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"module": "esnext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "build",
"rootDir": "src",
"noEmit": true,
"allowJs": true,
"checkJs": false,
"resolveJsonModule": true,
"incremental": true
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}
}

View File

@@ -7,5 +7,6 @@ export default defineConfig({
outDir: "build",
clean: true,
splitting: false,
noExternal: ["@apexfusion/vector-mcp-shared"],
banner: { js: "#!/usr/bin/env node" },
});

View File

@@ -0,0 +1,17 @@
{
"name": "@apexfusion/vector-mcp-shared",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
"./tx": "./src/tx.ts",
"./types": "./src/types.ts",
"./config": "./src/config.ts",
"./provider": "./src/provider.ts"
},
"dependencies": {
"@lucid-evolution/lucid": "^0.4.29",
"@noble/hashes": "^1.7.1",
"cross-fetch": "^4.0.0"
}
}

View File

@@ -0,0 +1,9 @@
// Vector network endpoints. Defaults target testnet; override via environment.
export const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vector.testnet.apexfusion.org';
export const VECTOR_SUBMIT_URL = process.env.VECTOR_SUBMIT_URL || 'https://submit.vector.testnet.apexfusion.org/api/submit/tx';
export const VECTOR_KOIOS_URL = process.env.VECTOR_KOIOS_URL || 'https://v2.koios.vector.testnet.apexfusion.org/';
export const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org';
export function explorerTxLink(txHash: string): string {
return `${VECTOR_EXPLORER_URL}/transaction/${txHash}`;
}

71
packages/shared/src/tx.ts Normal file
View File

@@ -0,0 +1,71 @@
// Pure transaction helpers. No network access — this module is safe to import
// from the signer, which must have no network capability.
import { blake2b } from '@noble/hashes/blake2b';
export function lovelaceToAda(lovelace: string | number | bigint): string {
if (lovelace === undefined || lovelace === null) return '0.000000';
try {
return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6);
} catch {
return '0.000000';
}
}
export function formatAssetName(name: string): string {
try {
if (/^[0-9a-fA-F]+$/.test(name) && name.length > 0) {
return Buffer.from(name, 'hex').toString('utf8');
}
return name;
} catch {
return name;
}
}
// Cardano metadata strings must be <= 64 bytes. Chunk long strings into arrays.
export function metadataStr(s: string): string | string[] {
if (s.length <= 64) return s;
const chunks: string[] = [];
for (let i = 0; i < s.length; i += 64) {
chunks.push(s.slice(i, i + 64));
}
return chunks;
}
export function cborUint(n: bigint): Buffer {
if (n < 0n) throw new Error('output_index must be non-negative');
if (n < 24n) return Buffer.from([Number(n)]);
if (n < 0x100n) return Buffer.from([0x18, Number(n)]);
if (n < 0x10000n) return Buffer.from([0x19, Number(n >> 8n) & 0xff, Number(n) & 0xff]);
if (n < 0x100000000n) {
const b = Buffer.alloc(5); b[0] = 0x1a; b.writeUInt32BE(Number(n), 1); return b;
}
const b = Buffer.alloc(9); b[0] = 0x1b; b.writeBigUInt64BE(n, 1); return b;
}
export function cborBytes(hex: string): Buffer {
const raw = Buffer.from(hex, 'hex');
const len = raw.length;
let header: Buffer;
if (len < 24) header = Buffer.from([0x40 | len]);
else if (len < 0x100) header = Buffer.from([0x58, len]);
else if (len < 0x10000) { header = Buffer.alloc(3); header[0] = 0x59; header.writeUInt16BE(len, 1); }
else { header = Buffer.alloc(5); header[0] = 0x5a; header.writeUInt32BE(len, 1); }
return Buffer.concat([header, raw]);
}
// NFT asset name = blake2b_256(CBOR(OutputReference)).
// agent-registry v2 (Conway) requires INDEFINITE-length CBOR for the constructor
// field array (D8 79 9F … FF), matching Aiken's cbor.serialise. Lucid's Data.to
// emits definite-length arrays (D8 79 82 …) which v2 rejects, so the outer
// constructor is hand-rolled here.
export function deriveNftAssetName(txHash: string, outputIndex: number): string {
const outRefCbor = Buffer.concat([
Buffer.from([0xd8, 0x79, 0x9f]),
cborBytes(txHash),
cborUint(BigInt(outputIndex)),
Buffer.from([0xff]),
]);
const hashBytes = blake2b(outRefCbor, { dkLen: 32 });
return Buffer.from(hashBytes).toString('hex');
}

View File

@@ -0,0 +1,41 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { deriveNftAssetName, lovelaceToAda, formatAssetName, metadataStr } from '../src/tx.ts';
const VECTORS: Array<{ tx: string; idx: number; expected: string }> = [
{ tx: '00'.repeat(32), idx: 0, expected: 'a2e5e227858e84f1a8f9b0c1246e6cbc9336d707d43ba43d0e1cb7c51c45f4c9' },
{ tx: '2703182e4d1151a32b7bcfc2362c91278bcac66a49a840d217ae5cb49f8b3649', idx: 0, expected: '24ed834d88890ce35e81a6b9c4f988fad3d8a0284db6f3e641416f44f5e95d75' },
{ tx: 'ff'.repeat(32), idx: 23, expected: 'df229a221c5a84f48c06d5b28a98deb6ea678c369ab166400e88dd3d2bdc7b9f' },
{ tx: 'ab'.repeat(32), idx: 24, expected: '3ddd07517205dd8a745887c33a09da0e4a5d63dca1f151ceeedaeeda0551c52b' },
{ tx: '01'.repeat(32), idx: 256, expected: 'e626a305779e4c1e74b7719d302d0fbc1b722ab33bd52da4c23e6b8cd1bd04ec' },
];
describe('deriveNftAssetName — Conway indefinite-CBOR parity', () => {
for (const { tx, idx, expected } of VECTORS) {
test(`tx=${tx.slice(0, 8)}… idx=${idx}`, () => {
assert.equal(deriveNftAssetName(tx, idx), expected);
});
}
});
describe('lovelaceToAda', () => {
test('converts whole ada', () => assert.equal(lovelaceToAda(2_000_000n), '2.000000'));
test('converts fractional ada', () => assert.equal(lovelaceToAda('1234567'), '1.234567'));
test('returns zero for null', () => assert.equal(lovelaceToAda(null as unknown as bigint), '0.000000'));
test('returns zero for garbage', () => assert.equal(lovelaceToAda('not-a-number'), '0.000000'));
});
describe('formatAssetName', () => {
test('decodes hex to utf8', () => assert.equal(formatAssetName('74657374'), 'test'));
test('passes through non-hex unchanged', () => assert.equal(formatAssetName('plain'), 'plain'));
test('passes through empty string', () => assert.equal(formatAssetName(''), ''));
});
describe('metadataStr', () => {
test('leaves short strings intact', () => assert.equal(metadataStr('short'), 'short'));
test('chunks strings longer than 64 bytes', () => {
const out = metadataStr('a'.repeat(150));
assert.ok(Array.isArray(out));
assert.deepEqual((out as string[]).map(s => s.length), [64, 64, 22]);
});
});

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true
},
"include": ["src/**/*", "test/**/*"]
}

View File

@@ -1,43 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { deriveNftAssetName } from '../../src/vector/agent-network.ts';
// Parity vectors — verified against pycardano OutputReference.to_cbor() and
// agent-sdk-py plutus_serialise_data(0, [tx_hash_bytes, idx]). All three
// implementations produce identical bytes for the v2 contract on Conway.
// See plan file for the verification run output.
const VECTORS: Array<{ tx: string; idx: number; expected: string }> = [
{
tx: '00'.repeat(32),
idx: 0,
expected: 'a2e5e227858e84f1a8f9b0c1246e6cbc9336d707d43ba43d0e1cb7c51c45f4c9',
},
{
tx: '2703182e4d1151a32b7bcfc2362c91278bcac66a49a840d217ae5cb49f8b3649',
idx: 0,
expected: '24ed834d88890ce35e81a6b9c4f988fad3d8a0284db6f3e641416f44f5e95d75',
},
{
tx: 'ff'.repeat(32),
idx: 23,
expected: 'df229a221c5a84f48c06d5b28a98deb6ea678c369ab166400e88dd3d2bdc7b9f',
},
{
tx: 'ab'.repeat(32),
idx: 24,
expected: '3ddd07517205dd8a745887c33a09da0e4a5d63dca1f151ceeedaeeda0551c52b',
},
{
tx: '01'.repeat(32),
idx: 256,
expected: 'e626a305779e4c1e74b7719d302d0fbc1b722ab33bd52da4c23e6b8cd1bd04ec',
},
];
describe('deriveNftAssetName — Conway indefinite-CBOR parity', () => {
for (const { tx, idx, expected } of VECTORS) {
test(`tx=${tx.slice(0, 8)}… idx=${idx}`, () => {
assert.equal(deriveNftAssetName(tx, idx), expected);
});
}
});