#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { CyberClient } from '@cybercongress/cyber-js';
import { CyberWallet } from './wallet.js';
import { CybMcpConfig, DEFAULT_CONFIG, BASE_DENOM } from './config.js';
import { isValidCID, getIpfsHash, fetchFromGateway, fetchContentFromGateway } from './utils.js';
interface ContentItem {
type: 'text' | 'image';
text?: string;
data?: string;
mimeType?: string;
}
class CybMcpServer {
private server: Server;
private wallet: CyberWallet;
private cyberClient: CyberClient | null = null;
private config: CybMcpConfig;
constructor(config: CybMcpConfig) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.server = new Server(
{
name: 'cyb-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.wallet = new CyberWallet(this.config);
// CyberClient will be initialized in the initialize method
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'sendCyberlink',
description: 'Create a cyberlink between two IPFS CIDs on the Cyber network',
inputSchema: {
type: 'object',
properties: {
from: {
type: 'string',
description: 'Source CID or text (will be converted to CID if not a valid CID)',
},
to: {
type: 'string',
description: 'Target CID or text (will be converted to CID if not a valid CID)',
},
fee: {
type: 'object',
description: 'Transaction fee (optional)',
properties: {
amount: {
type: 'array',
items: {
type: 'object',
properties: {
denom: { type: 'string' },
amount: { type: 'string' },
},
},
},
gas: { type: 'string' },
},
default: {
amount: [{ denom: BASE_DENOM, amount: '0' }],
gas: '200000',
},
},
},
required: ['from', 'to'],
},
},
{
name: 'searchQuery',
description: 'Search the Cyber knowledge graph and optionally retrieve content from IPFS',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (CID or text)',
},
page: {
type: 'number',
description: 'Page number for pagination (default: 0)',
default: 0,
},
retrieveContent: {
type: 'boolean',
description: 'Whether to retrieve actual content from IPFS gateway (default: false)',
default: false,
},
limit: {
type: 'number',
description: 'Maximum number of results to retrieve content for (default: 5)',
default: 5,
},
},
required: ['query'],
},
},
{
name: 'getCyberlink',
description: 'Retrieve content from IPFS by CID through the Cyber gateway',
inputSchema: {
type: 'object',
properties: {
cid: {
type: 'string',
description: 'IPFS CID to retrieve content for',
},
},
required: ['cid'],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'sendCyberlink':
return await this.handleSendCyberlink(args as any);
case 'searchQuery':
return await this.handleSearchQuery(args as any);
case 'getCyberlink':
return await this.handleGetCyberlink(args as any);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
private async handleSendCyberlink(args: {
from: string;
to: string;
fee?: any;
}) {
try {
// Check if wallet is initialized
if (!this.wallet.isInitialized()) {
return {
content: [
{
type: 'text',
text: 'Error: No mnemonic provided. The sendCyberlink tool requires a wallet to sign transactions. Please provide CYBER_MNEMONIC in your environment variables.',
},
],
isError: true,
};
}
// Convert to CIDs if necessary
const fromCid = isValidCID(args.from) ? args.from : await getIpfsHash(args.from);
const toCid = isValidCID(args.to) ? args.to : await getIpfsHash(args.to);
const signingClient = this.wallet.getSigningClient();
const address = this.wallet.getAddress();
const fee = args.fee || {
amount: [{ denom: BASE_DENOM, amount: '0' }],
gas: '200000',
};
const result = await signingClient.cyberlink(
address,
fromCid,
toCid,
fee
);
return {
content: [
{
type: 'text',
text: `Cyberlink created successfully!\nTransaction Hash: ${Array.isArray(result) ? 'Multiple transactions' : (result as any).transactionHash || 'Unknown'}\nFrom: ${fromCid}\nTo: ${toCid}`,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating cyberlink: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
private async handleSearchQuery(args: {
query: string;
page?: number;
retrieveContent?: boolean;
limit?: number;
}) {
try {
// Convert query to CID if necessary
const queryCid = isValidCID(args.query) ? args.query : await getIpfsHash(args.query);
const page = args.page || 0;
const retrieveContent = args.retrieveContent || false;
const limit = args.limit || 5;
if (!this.cyberClient) {
throw new Error('Cyber client not initialized');
}
let results;
try {
results = await this.cyberClient.search(queryCid, page);
} catch (searchError: any) {
// If particle not found, show the query and its hash
if (searchError?.message?.includes('particle not found')) {
return {
content: [{
type: 'text',
text: `No particle found for query: "${args.query}"\nCalculated hash: ${queryCid}\n\nThis means no cyberlinks have been created from this particle yet.`
}],
};
}
throw searchError;
}
const contentItems: ContentItem[] = [];
// Add header with search info
contentItems.push({
type: 'text',
text: `Search results for: ${args.query}\nQuery CID: ${queryCid}\n\nFound ${results.result?.length || 0} cyberlinks`,
});
if (!results || !results.result || results.result.length === 0) {
return { content: contentItems };
}
// Process each result
for (let i = 0; i < results.result.length; i++) {
const result = results.result[i];
// Add result info
contentItems.push({
type: 'text',
text: `\n--- Result ${i + 1} ---\nCID: ${result.particle}\nRank: ${result.rank || 'N/A'}`,
});
// Retrieve content if requested and within limit
if (retrieveContent && i < limit) {
const contentItem = await this.retrieveContentByCID(result.particle);
// Add prefix for context
if (contentItem.type === 'text' && contentItem.text && !contentItem.text.startsWith('Error')) {
contentItem.text = `Content:\n${contentItem.text.length > 200 ? contentItem.text.substring(0, 200) + '...' : contentItem.text}`;
}
contentItems.push(contentItem);
}
}
return { content: contentItems };
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error searching: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
private async retrieveContentByCID(cid: string): Promise<ContentItem> {
try {
const { content, mimeType, isImage } = await fetchContentFromGateway(
this.config.cyberGateway,
cid
);
if (isImage) {
return {
type: 'image',
data: content, // base64 encoded image data
mimeType,
};
} else {
return {
type: 'text',
text: content,
};
}
} catch (error) {
return {
type: 'text',
text: `Error retrieving content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
private async handleGetCyberlink(args: { cid: string }) {
try {
if (!isValidCID(args.cid)) {
return {
content: [
{
type: 'text',
text: `Invalid CID format: ${args.cid}`,
},
],
isError: true,
};
}
const contentItem = await this.retrieveContentByCID(args.cid);
// Add CID info for text content
if (contentItem.type === 'text' && contentItem.text && !contentItem.text.startsWith('Error')) {
contentItem.text = `Content from CID: ${args.cid}\n\n${contentItem.text}`;
}
return {
content: [contentItem],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error retrieving content for CID ${args.cid}: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
async initialize(): Promise<void> {
// Initialize wallet (will be skipped if no mnemonic provided)
await this.wallet.initialize();
// Always initialize cyber client for read operations
this.cyberClient = await CyberClient.connect(this.config.rpcUrl);
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}
// Main execution
async function main() {
// Load configuration from environment
const config: Partial<CybMcpConfig> = {
mnemonic: process.env.CYBER_MNEMONIC, // Optional - undefined if not provided
rpcUrl: process.env.CYBER_RPC_URL,
cyberGateway: process.env.CYBER_GATEWAY,
};
const server = new CybMcpServer(config as CybMcpConfig);
try {
await server.initialize();
await server.run();
} catch (error) {
console.error('Failed to start MCP server:', error);
process.exit(1);
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error('Unhandled error:', error);
process.exit(1);
});
}