Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,557 changes: 2,075 additions & 1,482 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,44 @@
"dev:watch": "tsc --watch",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"format": "prettier --write \"**/*.{ts,js,json,md}\""
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"author": "Overclock Labs, Inc.",
"license": "Apache-2.0",
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"@akashnetwork/akash-api": "^1.4.0",
"@akashnetwork/akashjs": "^0.11.1",
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@akashnetwork/chain-sdk": "^1.0.0-alpha.18",
"@cosmjs/proto-signing": "^0.33.1",
"@cosmjs/stargate": "^0.33.1",
"@modelcontextprotocol/sdk": "^1.8.0",
"fs": "0.0.1-security",
"https": "^1.0.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"path": "^0.12.7",
"ws": "^8.18.3",
"zod": "^3.22.4"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.14",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"@vitest/coverage-v8": "^4.0.13",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"prettier": "^3.5.3",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0"
"typescript-eslint": "^8.29.0",
"vitest": "^4.0.13"
}
}
111 changes: 88 additions & 23 deletions src/AkashMCP.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import type { SigningStargateClient } from '@cosmjs/stargate';
import { loadWalletAndClient, loadCertificate } from './utils/index.js';
import type { CertificatePem } from '@akashnetwork/chain-sdk';
import { loadWalletAndClient, loadCertificate, loadCertificateFromDisk } from './utils/index.js';
import { SERVER_CONFIG } from './config.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
Expand All @@ -17,14 +17,19 @@ import {
GetBalancesTool,
CloseDeploymentTool,
GetDeploymentTool,
RevokeCertificateTool,
RevokeAllCertificatesTool,
RegenerateCertificateTool,
GetLogsTool,
ExecCommandTool,
} from './tools/index.js';
import type { ToolContext } from './types/index.js';
import type { CertificatePem } from '@akashnetwork/akashjs/build/certificates/certificate-manager/CertificateManager.js';
import type { ToolContext, ChainNodeSDK, StargateTxClient } from './types/index.js';

class AkashMCP extends McpServer {
private wallet: DirectSecp256k1HdWallet | null = null;
private client: SigningStargateClient | null = null;
private client: StargateTxClient | null = null;
private certificate: CertificatePem | null = null;
private chainSDK: ChainNodeSDK | null = null;

constructor() {
super({
Expand All @@ -33,18 +38,41 @@ class AkashMCP extends McpServer {
});
}

private getToolContext(): ToolContext {
private async getToolContext(): Promise<ToolContext> {
if (!this.isInitialized()) {
throw new Error('MCP server not initialized');
}

// Always read certificate fresh from disk to ensure we have the latest
const accounts = await this.wallet!.getAccounts();
const freshCert = loadCertificateFromDisk(accounts[0].address);
if (freshCert) {
this.certificate = freshCert;
} else if (!this.certificate) {
throw new Error(
'No certificate available. The on-disk certificate is missing and no in-memory ' +
'certificate exists. Run regenerate-certificate to create a new one.'
);
}

return {
client: this.client!,
wallet: this.wallet!,
certificate: this.certificate!,
chainSDK: this.chainSDK!,
reloadCertificate: this.reloadCertificate.bind(this),
};
}

public getClient(): SigningStargateClient {
public async reloadCertificate(): Promise<CertificatePem> {
if (!this.wallet || !this.client || !this.chainSDK) {
throw new Error('Cannot reload certificate: server not initialized');
}
this.certificate = await loadCertificate(this.wallet, this.chainSDK);
return this.certificate;
}

public getClient(): StargateTxClient {
if (!this.client) {
throw new Error('Client not initialized');
}
Expand All @@ -53,10 +81,11 @@ class AkashMCP extends McpServer {

public async initialize() {
try {
const { wallet, client } = await loadWalletAndClient();
const { wallet, client, chainSDK } = await loadWalletAndClient();
this.wallet = wallet;
this.client = client;
this.certificate = await loadCertificate(wallet, client);
this.chainSDK = chainSDK;
this.certificate = await loadCertificate(wallet, chainSDK);
} catch (error) {
console.error('Failed to initialize MCP server:', error);
throw error;
Expand All @@ -68,95 +97,131 @@ class AkashMCP extends McpServer {
GetAccountAddrTool.name,
GetAccountAddrTool.description,
GetAccountAddrTool.parameters.shape,
async (args, extra) => GetAccountAddrTool.handler(args, this.getToolContext())
async (args) => GetAccountAddrTool.handler(args, await this.getToolContext())
);

this.tool(
GetBidsTool.name,
GetBidsTool.description,
GetBidsTool.parameters.shape,
async (args, extra) => GetBidsTool.handler(args, this.getToolContext())
async (args) => GetBidsTool.handler(args, await this.getToolContext())
);

this.tool(
CreateDeploymentTool.name,
CreateDeploymentTool.description,
CreateDeploymentTool.parameters.shape,
async (args, extra) => CreateDeploymentTool.handler(args, this.getToolContext())
async (args) => CreateDeploymentTool.handler(args, await this.getToolContext())
);

this.tool(
GetSDLsTool.name,
GetSDLsTool.description,
GetSDLsTool.parameters.shape,
async (args, extra) => GetSDLsTool.handler(args, this.getToolContext())
async (args) => GetSDLsTool.handler(args, await this.getToolContext())
);

this.tool(
GetSDLTool.name,
GetSDLTool.description,
GetSDLTool.parameters.shape,
async (args, extra) => GetSDLTool.handler(args, this.getToolContext())
async (args) => GetSDLTool.handler(args, await this.getToolContext())
);

this.tool(
SendManifestTool.name,
SendManifestTool.description,
SendManifestTool.parameters.shape,
async (args, extra) => SendManifestTool.handler(args, this.getToolContext())
async (args) => SendManifestTool.handler(args, await this.getToolContext())
);

this.tool(
CreateLeaseTool.name,
CreateLeaseTool.description,
CreateLeaseTool.parameters.shape,
async (args, extra) => CreateLeaseTool.handler(args, this.getToolContext())
async (args) => CreateLeaseTool.handler(args, await this.getToolContext())
);

this.tool(
GetServicesTool.name,
GetServicesTool.description,
GetServicesTool.parameters.shape,
async (args, extra) => GetServicesTool.handler(args, this.getToolContext())
async (args) => GetServicesTool.handler(args, await this.getToolContext())
);

this.tool(
UpdateDeploymentTool.name,
UpdateDeploymentTool.description,
UpdateDeploymentTool.parameters.shape,
async (args, extra) => UpdateDeploymentTool.handler(args, this.getToolContext())
async (args) => UpdateDeploymentTool.handler(args, await this.getToolContext())
);

this.tool(
AddFundsTool.name,
AddFundsTool.description,
AddFundsTool.parameters.shape,
async (args, extra) => AddFundsTool.handler(args, this.getToolContext())
async (args) => AddFundsTool.handler(args, await this.getToolContext())
);

this.tool(
GetBalancesTool.name,
GetBalancesTool.description,
GetBalancesTool.parameters.shape,
async (args, extra) => GetBalancesTool.handler(args, this.getToolContext())
async (args) => GetBalancesTool.handler(args, await this.getToolContext())
);

this.tool(
CloseDeploymentTool.name,
CloseDeploymentTool.description,
CloseDeploymentTool.parameters.shape,
async (args, extra) => CloseDeploymentTool.handler(args, this.getToolContext())
async (args) => CloseDeploymentTool.handler(args, await this.getToolContext())
);

this.tool(
GetDeploymentTool.name,
GetDeploymentTool.description,
GetDeploymentTool.parameters.shape,
async (args, extra) => GetDeploymentTool.handler(args, this.getToolContext())
async (args) => GetDeploymentTool.handler(args, await this.getToolContext())
);

this.tool(
RevokeCertificateTool.name,
RevokeCertificateTool.description,
RevokeCertificateTool.parameters.shape,
async (args) => RevokeCertificateTool.handler(args, await this.getToolContext())
);

this.tool(
RevokeAllCertificatesTool.name,
RevokeAllCertificatesTool.description,
RevokeAllCertificatesTool.parameters.shape,
async (args) => RevokeAllCertificatesTool.handler(args, await this.getToolContext())
);

this.tool(
RegenerateCertificateTool.name,
RegenerateCertificateTool.description,
RegenerateCertificateTool.parameters.shape,
async (args) => RegenerateCertificateTool.handler(args, await this.getToolContext())
);

this.tool(
GetLogsTool.name,
GetLogsTool.description,
GetLogsTool.parameters.shape,
async (args) => GetLogsTool.handler(args, await this.getToolContext())
);

this.tool(
ExecCommandTool.name,
ExecCommandTool.description,
ExecCommandTool.parameters.shape,
async (args) => ExecCommandTool.handler(args, await this.getToolContext())
);

}
public isInitialized(): boolean {
return this.wallet !== null && this.client !== null && this.certificate !== null;
return this.wallet !== null && this.client !== null && this.certificate !== null && this.chainSDK !== null;
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,27 @@ export const SERVER_CONFIG = {
port: process.env.PORT || 3000,
environment: process.env.NODE_ENV || 'development',
rpcEndpoint: process.env.RPC_ENDPOINT || 'https://rpc.akashnet.net:443',
grpcEndpoint: process.env.GRPC_ENDPOINT || 'https://akash-grpc.publicnode.com:443',
mnemonic: process.env.AKASH_MNEMONIC || '',
} as const;

export type ServerConfig = typeof SERVER_CONFIG;

/** Validate mnemonic before wallet creation. Call at startup, not at import. */
export function validateMnemonic(mnemonic: string): string {
if (!mnemonic || mnemonic.trim().length === 0) {
throw new Error(
'AKASH_MNEMONIC environment variable is required. ' +
'Set it to your 12 or 24 word BIP-39 mnemonic.'
);
}

const words = mnemonic.trim().split(/\s+/);
if (words.length !== 12 && words.length !== 24) {
throw new Error(
`AKASH_MNEMONIC must be 12 or 24 words, got ${words.length}.`
);
}

return mnemonic.trim();
}
53 changes: 28 additions & 25 deletions src/tools/add-funds.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { z } from 'zod';
import type { ToolDefinition, ToolContext } from '../types/index.js';
import { createOutput } from '../utils/create-output.js';
import { getTypeUrl } from '@akashnetwork/akashjs/build/stargate/index.js';
import { MsgDepositDeployment } from '@akashnetwork/akash-api/akash/deployment/v1beta3';
import { QueryClientImpl, QueryDeploymentRequest } from '@akashnetwork/akash-api/akash/deployment/v1beta3';
import { getRpc } from '@akashnetwork/akashjs/build/rpc/index.js';
import { SERVER_CONFIG } from '../config.js';

const parameters = z.object({
address: z.string().min(1, 'Akash account address is required'),
Expand All @@ -19,34 +14,42 @@ export const AddFundsTool: ToolDefinition<typeof parameters> = {
parameters,
handler: async (params, context) => {
const { address, dseq, amount } = params;
const { chainSDK } = context;

try {
// 1. Validate deployment exists
const rpc = await getRpc(SERVER_CONFIG.rpcEndpoint);
const deploymentClient = new QueryClientImpl(rpc);
const queryReq = QueryDeploymentRequest.fromPartial({
id: { owner: address, dseq },
const deploymentRes = await chainSDK.akash.deployment.v1beta4.getDeployment({
id: {
owner: address,
dseq: BigInt(dseq),
},
});
const deploymentRes = await deploymentClient.Deployment(queryReq);

if (!deploymentRes.deployment) {
return createOutput({ error: `Deployment with owner ${address} and dseq ${dseq} not found.` });
}

// 2. Prepare MsgDepositDeployment
const depositMsg = MsgDepositDeployment.fromPartial({
id: { owner: address, dseq },
amount: { denom: 'uakt', amount: amount.toString() },
depositor: address,
// 2. Deposit funds using escrow accountDeposit
// The escrow account xid is typically the deployment ID in a specific format
// Format: owner/dseq
const result = await chainSDK.akash.escrow.v1.accountDeposit({
signer: address,
id: {
scope: 1, // deployment scope
xid: `${address}/${dseq}`,
},
deposit: {
amount: { denom: 'uakt', amount: amount.toString() },
sources: [1], // Source.balance = 1
},
});
const msg = {
typeUrl: getTypeUrl(MsgDepositDeployment),
value: depositMsg,
};

// 3. Sign and broadcast
const tx = await context.client.signAndBroadcast(address, [msg], 'auto');
return createOutput(tx.rawLog);
} catch (error: any) {
return createOutput({ error: error.message || 'Failed to add funds to deployment.' });
return createOutput({
success: true,
result: result,
});
} catch (error: unknown) {
return createOutput({ error: error instanceof Error ? error.message : String(error) || 'Failed to add funds to deployment.' });
}
},
};
};
Loading
Loading