import fs from "fs";
import path from "path";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { wrapFetchWithPayment, createKeypairSigner } from "x402-stellar-fetch";
import { Keypair } from "@stellar/stellar-sdk";
/* -----------------------------
Marketplace backend
-------------------------------- */
const MARKETPLACE_URL = "https://marketplacebackend-xlca.onrender.com";
/* -----------------------------
Persistent server wallet
-------------------------------- */
const HOME = process.env.HOME!;
const DATA_DIR =
process.env.XDG_STATE_HOME
? path.join(process.env.XDG_STATE_HOME, "mcp")
: path.join(HOME, ".local", "state", "mcp");
const WALLET_FILE = path.join(DATA_DIR, "stellar-wallet.json");
async function loadOrCreateServerWallet(): Promise<Keypair> {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
if (fs.existsSync(WALLET_FILE)) {
const raw = JSON.parse(fs.readFileSync(WALLET_FILE, "utf8"));
return Keypair.fromSecret(raw.secret);
}
const keypair = Keypair.random();
fs.writeFileSync(
WALLET_FILE,
JSON.stringify(
{
secret: keypair.secret(),
public: keypair.publicKey(),
createdAt: new Date().toISOString(),
},
null,
2
),
{ mode: 0o600 }
);
console.log("[MCP] Created server wallet:", keypair.publicKey());
// Testnet funding
const url = `https://friendbot.stellar.org/?addr=${encodeURIComponent(
keypair.publicKey()
)}`;
await fetch(url);
return keypair;
}
/* -----------------------------
Bootstrap payment fetch
-------------------------------- */
const serverKeypair = await loadOrCreateServerWallet();
const signer = createKeypairSigner(serverKeypair);
const fetchWithPay = wrapFetchWithPayment(fetch, signer, {
maxAmount: BigInt(50_000_000),
});
/* -----------------------------
MCP server
-------------------------------- */
const server = new McpServer({
name: "stellar-market",
version: "0.1.0",
});
/* -----------------------------
MCP tool: call_paid_tool
-------------------------------- */
server.registerTool(
"call_paid_tool",
{
description: "Call a paid tool from the marketplace. Payment is handled automatically.",
inputSchema: {
tool: z.string().describe("Tool name, example: get_weather"),
category: z.string().describe("Tool category"),
args: z.record(z.any()).describe("Tool arguments"),
},
},
async ({ tool, args }) => {
const url = `${MARKETPLACE_URL}/tools/${tool}`;
const res = await fetchWithPay(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(args),
});
if (!res.ok) {
const text = await res.text();
return {
content: [
{
type: "text",
text: `Tool call failed (${res.status}): ${text}`,
},
],
};
}
const data = await res.json();
return {
content: [
{
type: "text",
text: JSON.stringify(data),
},
],
};
}
);
/* -----------------------------
MCP tool: marketplace_list_tools
-------------------------------- */
server.registerTool(
"marketplace_list_tools",
{
description: "List all available marketplace tools with pricing and parameters",
inputSchema: z.object({}).strict(),
},
async () => {
const res = await fetch(`${MARKETPLACE_URL}/tools/info`);
if (!res.ok) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: "Failed to fetch marketplace tools" }),
},
],
};
}
const tools = await res.json();
return {
content: [
{
type: "text",
text: JSON.stringify({ tools }, null, 2),
},
],
};
}
);
/* -----------------------------
Start MCP server
-------------------------------- */
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Marketplace stellar MCP server started");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});