Skip to content
Open
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
174 changes: 103 additions & 71 deletions app/mcp/route.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,124 @@
import { createMcpHandler } from "mcp-handler";
import { NextResponse } from "next/server";
import { z } from "zod";
import { createMcpHandler } from "mcp-handler"
import { NextResponse } from "next/server"
import { z } from "zod"
import { isAddress } from "viem"
import {
getRitualWalletBalance,
getActiveExecutors,
getChainStatus,
getAvailableModels,
CAPABILITY,
} from "@/lib/ritual-chain"

// Tool definitions for reuse in GET response
const tools = [
{
name: "echo",
description: "Echo back a message to verify input/output communication",
parameters: { message: "string" },
},
{
name: "get_server_time",
description: "Returns the current server time and timezone information",
parameters: {},
},
];

// StreamableHttp server
const handler = createMcpHandler(
async (server) => {
server.tool(
"echo",
"Echo back a message to verify input/output communication",
{
message: z.string(),
},
{ message: z.string() },
async ({ message }) => ({
content: [{ type: "text", text: `Tool echo: ${message}` }],
})
);
)

server.tool(
"get_server_time",
"Returns the current server time and timezone information",
{},
async () => ({
content: [
{
type: "text",
text: JSON.stringify(
{
serverTime: new Date().toISOString(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: Date.now(),
},
null,
2
),
},
],
content: [{ type: "text", text: JSON.stringify({ serverTime: new Date().toISOString(), timestamp: Date.now() }, null, 2) }],
})
);
},
{
capabilities: {
tools: {},
},
)

server.tool(
"get_ritual_chain_status",
"Returns Ritual Chain (Chain ID 1979) status: block number, gas price, RPC/explorer/faucet URLs. Call this first to verify connectivity before submitting any transaction.",
{},
async () => {
try {
const status = await getChainStatus()
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }
} catch (err) {
return { content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }) }], isError: true }
}
}
)

server.tool(
"get_ritual_wallet_balance",
"Returns RitualWallet balance and lock status for an address on Ritual Chain. RitualWallet funds async precompile calls (HTTP 0x0801, LLM 0x0802, agents). Minimum 0.4 RITUAL required before LLM calls. hasEnoughForLLM field indicates readiness.",
{ address: z.string().describe("Ethereum address (0x...) to check") },
async ({ address }) => {
if (!isAddress(address)) {
return { content: [{ type: "text", text: JSON.stringify({ error: "Invalid Ethereum address" }) }], isError: true }
}
try {
const result = await getRitualWalletBalance(address as `0x${string}`)
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }
} catch (err) {
return { content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }) }], isError: true }
}
}
)

server.tool(
"get_ritual_executors",
"Returns active TEE executor addresses from TEEServiceRegistry for HTTP or LLM precompile. Pass the returned teeAddress as the executor field in precompile ABI calls. capability: http=0x0801, llm=0x0802.",
{ capability: z.enum(["http", "llm"]).describe("http for HTTP precompile (0x0801), llm for LLM precompile (0x0802)") },
async ({ capability }) => {
try {
const cap = capability === "http" ? CAPABILITY.HTTP_CALL : CAPABILITY.LLM_CALL
const executors = await getActiveExecutors(cap)
return {
content: [{
type: "text",
text: JSON.stringify({
capability,
precompile: capability === "http" ? "0x0801" : "0x0802",
count: executors.length,
executors,
note: "Use teeAddress as the executor parameter in precompile calls",
}, null, 2),
}],
}
} catch (err) {
return { content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }) }], isError: true }
}
}
)

server.tool(
"get_ritual_models",
"Returns registered AI models from ModelPricingRegistry on Ritual Chain. Only zai-org/GLM-4.7-FP8 is live for production. Always pin to productionModel for LLM precompile calls.",
{},
async () => {
try {
const models = await getAvailableModels()
return {
content: [{
type: "text",
text: JSON.stringify({ models, productionModel: "zai-org/GLM-4.7-FP8", note: "Pin to productionModel for all current Ritual Chain LLM calls" }, null, 2),
}],
}
} catch (err) {
return { content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }) }], isError: true }
}
}
)
},
{
basePath: "",
verboseLogs: true,
maxDuration: 60,
disableSse: true,
}
);
{ capabilities: { tools: {} } },
{ basePath: "", verboseLogs: true, maxDuration: 60, disableSse: true }
)

// GET handler — returns server info for browsers/discovery
export async function GET() {
return NextResponse.json(
{
name: "MCP Server Template",
version: "0.1.0",
protocol: "Model Context Protocol (MCP)",
transport: "Streamable HTTP",
status: "running",
tools,
usage: {
note: "This endpoint uses POST for MCP communication",
test: "npx tsx scripts/test-streamable-http-client.ts <origin>",
docs: "https://modelcontextprotocol.io",
},
},
{
headers: {
"Content-Type": "application/json",
},
}
);
return NextResponse.json({
name: "Ritual Chain MCP Server",
version: "0.2.0",
protocol: "Model Context Protocol (MCP)",
transport: "Streamable HTTP",
chain: { id: 1979, name: "Ritual", rpc: "https://rpc.ritualfoundation.org", explorer: "https://explorer.ritualfoundation.org" },
tools: ["echo", "get_server_time", "get_ritual_chain_status", "get_ritual_wallet_balance", "get_ritual_executors", "get_ritual_models"],
})
}

export { handler as POST, handler as DELETE };
export { handler as POST, handler as DELETE }
115 changes: 115 additions & 0 deletions lib/ritual-chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { createPublicClient, http, defineChain, formatEther, type Address } from "viem"

export const ritualChain = defineChain({
id: 1979,
name: "Ritual",
nativeCurrency: { name: "RITUAL", symbol: "RITUAL", decimals: 18 },
rpcUrls: {
default: { http: ["https://rpc.ritualfoundation.org"] },
},
blockExplorers: {
default: { name: "Ritual Explorer", url: "https://explorer.ritualfoundation.org" },
},
})

export const publicClient = createPublicClient({
chain: ritualChain,
transport: http("https://rpc.ritualfoundation.org"),
})

export const RITUAL_WALLET = "0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948" as const
export const TEE_SERVICE_REGISTRY = "0x9644e8562cE0Fe12b4deeC4163c064A8862Bf47F" as const
export const MODEL_PRICING_REGISTRY = "0x7A85F48b971ceBb75491b61abe279728F4c4384f" as const

export const CAPABILITY = { HTTP_CALL: 0, LLM_CALL: 1 } as const

const RITUAL_WALLET_ABI = [
{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] },
{ name: "lockUntil", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] },
] as const

const TEE_REGISTRY_ABI = [
{
name: "getServicesByCapability",
type: "function",
stateMutability: "view",
inputs: [{ name: "capability", type: "uint8" }, { name: "checkValidity", type: "bool" }],
outputs: [{
name: "services", type: "tuple[]",
components: [
{ name: "node", type: "tuple", components: [
{ name: "paymentAddress", type: "address" },
{ name: "teeAddress", type: "address" },
{ name: "teeType", type: "uint8" },
{ name: "publicKey", type: "bytes" },
{ name: "endpoint", type: "string" },
{ name: "certPubKeyHash", type: "bytes32" },
{ name: "capability", type: "uint8" },
]},
{ name: "isValid", type: "bool" },
{ name: "workloadId", type: "bytes32" },
],
}],
},
] as const

const MODEL_PRICING_ABI = [
{ name: "getAllModels", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string[]" }] },
] as const

export async function getRitualWalletBalance(address: Address) {
const [balance, lockUntil, currentBlock] = await Promise.all([
publicClient.readContract({ address: RITUAL_WALLET, abi: RITUAL_WALLET_ABI, functionName: "balanceOf", args: [address] }),
publicClient.readContract({ address: RITUAL_WALLET, abi: RITUAL_WALLET_ABI, functionName: "lockUntil", args: [address] }),
publicClient.getBlockNumber(),
])
return {
address,
balanceWei: balance.toString(),
balanceRitual: formatEther(balance),
lockUntilBlock: lockUntil.toString(),
currentBlock: currentBlock.toString(),
isLocked: lockUntil > currentBlock,
hasEnoughForLLM: balance >= BigInt("400000000000000000"),
}
}

export async function getActiveExecutors(capability: 0 | 1) {
const services = await publicClient.readContract({
address: TEE_SERVICE_REGISTRY,
abi: TEE_REGISTRY_ABI,
functionName: "getServicesByCapability",
args: [capability, true],
})
return services.map((s) => ({
teeAddress: s.node.teeAddress,
paymentAddress: s.node.paymentAddress,
isValid: s.isValid,
}))
}

export async function getChainStatus() {
const [blockNumber, gasPrice] = await Promise.all([
publicClient.getBlockNumber(),
publicClient.getGasPrice(),
])
return {
chainId: 1979,
chainName: "Ritual",
rpcUrl: "https://rpc.ritualfoundation.org",
explorerUrl: "https://explorer.ritualfoundation.org",
faucetUrl: "https://faucet.ritualfoundation.org",
blockNumber: blockNumber.toString(),
gasPriceWei: gasPrice.toString(),
gasPriceGwei: (Number(gasPrice) / 1e9).toFixed(4),
}
}

export async function getAvailableModels() {
const models = await publicClient.readContract({
address: MODEL_PRICING_REGISTRY,
abi: MODEL_PRICING_ABI,
functionName: "getAllModels",
})
return models as string[]
}
Loading