refactor: add workspaces root and @apexfusion/vector-mcp-shared
Extracts pure tx helpers, shared types, endpoint config, and the Ogmios provider. Subpath exports (./tx vs ./provider) let the forthcoming signer import pure helpers while being structurally unable to reach the network layer.
This commit is contained in:
73
package.json
73
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/shared/package.json
Normal file
17
packages/shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
9
packages/shared/src/config.ts
Normal file
9
packages/shared/src/config.ts
Normal 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://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
71
packages/shared/src/tx.ts
Normal 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');
|
||||
}
|
||||
41
packages/shared/test/tx.test.ts
Normal file
41
packages/shared/test/tx.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
15
packages/shared/tsconfig.json
Normal file
15
packages/shared/tsconfig.json
Normal 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/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user