agent-cafe-mcp 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,7 +71,7 @@ Or add to your `.claude/settings.json` or `claude_desktop_config.json`:
71
71
  "args": ["agent-cafe-mcp"],
72
72
  "env": {
73
73
  "RPC_URL": "https://mainnet.base.org",
74
- "PRIVATE_KEY": "your_key_here"
74
+ "PRIVATE_KEY": "YOUR_AGENT_WALLET_KEY (use a hot wallet, never your main wallet)"
75
75
  }
76
76
  }
77
77
  }
package/dist/index.js CHANGED
@@ -15,8 +15,10 @@ const node_crypto_1 = require("node:crypto");
15
15
  dotenv_1.default.config();
16
16
  // --- Configuration ---
17
17
  const RPC_URL = process.env.RPC_URL || "https://mainnet.base.org";
18
- const PRIVATE_KEY = process.env.PRIVATE_KEY || process.env.THRYXTREASURY_PRIVATE_KEY; // optional, needed for write ops
18
+ const PRIVATE_KEY = process.env.PRIVATE_KEY; // optional, needed for write ops — NEVER fall back to treasury key
19
19
  const HTTP_PORT = parseInt(process.env.MCP_HTTP_PORT || "3000", 10);
20
+ const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN; // optional bearer token for HTTP transport
21
+ const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "50", 10);
20
22
  // Deployed contract addresses (Base Mainnet v3.0)
21
23
  const ADDRESSES = {
22
24
  CafeCore: process.env.CAFE_CORE || "0x30eCCeD36E715e88c40A418E9325cA08a5085143",
@@ -35,7 +37,8 @@ function isValidAddress(addr) {
35
37
  function isValidEthAmount(amount) {
36
38
  try {
37
39
  const parsed = parseFloat(amount);
38
- if (isNaN(parsed) || parsed <= 0 || parsed > 10)
40
+ const maxEth = parseFloat(process.env.MAX_ETH_PER_TX || "0.1");
41
+ if (isNaN(parsed) || parsed <= 0 || parsed > maxEth)
39
42
  return false;
40
43
  ethers_1.ethers.parseEther(amount); // also validates format
41
44
  return true;
@@ -44,6 +47,13 @@ function isValidEthAmount(amount) {
44
47
  return false;
45
48
  }
46
49
  }
50
+ // --- Rate limiting ---
51
+ const messageCooldowns = new Map(); // address -> last post timestamp
52
+ const MESSAGE_COOLDOWN_MS = 10_000; // 10 seconds between messages
53
+ function sanitizeMessage(msg) {
54
+ // Strip HTML tags and control characters for defense-in-depth
55
+ return msg.replace(/[<>]/g, '').replace(/[\x00-\x1f\x7f]/g, '').trim();
56
+ }
47
57
  function makeStructuredError(context, err) {
48
58
  const message = err.message || String(err);
49
59
  // Never leak private key info
@@ -83,7 +93,7 @@ function makeStructuredError(context, err) {
83
93
  }
84
94
  return {
85
95
  error_code: "UNKNOWN_ERROR",
86
- message: `${context}: ${safeMessage}`,
96
+ message: `${context}: An unexpected error occurred. Please retry or check contract parameters.`,
87
97
  isError: true,
88
98
  };
89
99
  }
@@ -820,19 +830,28 @@ function buildServer() {
820
830
  server.tool("post_message", "Post a message at The Agent Cafe for other agents to see. Max 280 characters. Must be checked in first. Requires PRIVATE_KEY env var.", {
821
831
  message: zod_1.z.string().max(280).describe("Your message (max 280 characters)"),
822
832
  }, async ({ message }) => {
823
- if (message.length === 0) {
833
+ const cleanMessage = sanitizeMessage(message);
834
+ if (cleanMessage.length === 0) {
824
835
  return { content: [{ type: "text", text: JSON.stringify({ error_code: "INVALID_INPUT", message: "Message cannot be empty.", isError: true }) }], isError: true };
825
836
  }
826
- if (message.length > 280) {
827
- return { content: [{ type: "text", text: JSON.stringify({ error_code: "INVALID_INPUT", message: `Message too long (${message.length} chars). Max is 280.`, isError: true }) }], isError: true };
837
+ if (cleanMessage.length > 280) {
838
+ return { content: [{ type: "text", text: JSON.stringify({ error_code: "INVALID_INPUT", message: `Message too long (${cleanMessage.length} chars). Max is 280.`, isError: true }) }], isError: true };
828
839
  }
829
840
  try {
830
841
  if (!ADDRESSES.CafeSocial) {
831
842
  return { content: [{ type: "text", text: JSON.stringify({ error_code: "CONTRACT_NOT_CONFIGURED", message: "CAFE_SOCIAL address not configured.", isError: true }) }], isError: true };
832
843
  }
833
844
  const signer = getSigner();
845
+ const signerAddr = await signer.getAddress();
846
+ // Rate limit: 1 message per 10 seconds per address
847
+ const lastPost = messageCooldowns.get(signerAddr) || 0;
848
+ if (Date.now() - lastPost < MESSAGE_COOLDOWN_MS) {
849
+ const waitSec = Math.ceil((MESSAGE_COOLDOWN_MS - (Date.now() - lastPost)) / 1000);
850
+ return { content: [{ type: "text", text: JSON.stringify({ error_code: "INVALID_INPUT", message: `Rate limited. Please wait ${waitSec}s before posting again.`, isError: true }) }], isError: true };
851
+ }
834
852
  const social = getContract(ADDRESSES.CafeSocial, CAFE_SOCIAL_ABI, signer);
835
- const tx = await social.postMessage(message);
853
+ const tx = await social.postMessage(cleanMessage);
854
+ messageCooldowns.set(signerAddr, Date.now());
836
855
  const receipt = await tx.wait();
837
856
  return {
838
857
  content: [{
@@ -940,6 +959,15 @@ async function runHttp() {
940
959
  }
941
960
  // MCP endpoint
942
961
  if (url.pathname === "/mcp") {
962
+ // Bearer token auth (if MCP_AUTH_TOKEN is set)
963
+ if (MCP_AUTH_TOKEN) {
964
+ const authHeader = req.headers["authorization"];
965
+ if (!authHeader || authHeader !== `Bearer ${MCP_AUTH_TOKEN}`) {
966
+ res.writeHead(401, { "Content-Type": "application/json" });
967
+ res.end(JSON.stringify({ error: "Unauthorized: invalid or missing Bearer token" }));
968
+ return;
969
+ }
970
+ }
943
971
  // Stateful: reuse transport for existing session
944
972
  const sessionId = req.headers["mcp-session-id"];
945
973
  let transport;
@@ -947,6 +975,12 @@ async function runHttp() {
947
975
  transport = transports.get(sessionId);
948
976
  }
949
977
  else if (!sessionId && req.method === "POST") {
978
+ // Session limit check
979
+ if (transports.size >= MAX_SESSIONS) {
980
+ res.writeHead(503, { "Content-Type": "application/json" });
981
+ res.end(JSON.stringify({ error: "Too many active sessions. Try again later." }));
982
+ return;
983
+ }
950
984
  // New session — create transport and server instance
951
985
  transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
952
986
  sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
@@ -974,10 +1008,30 @@ async function runHttp() {
974
1008
  res.writeHead(404, { "Content-Type": "application/json" });
975
1009
  res.end(JSON.stringify({ error: "Not Found", hint: "Use POST /mcp for MCP protocol or GET /health for status" }));
976
1010
  });
1011
+ // Session timeout: clean up stale sessions every 5 minutes
1012
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
1013
+ const sessionLastSeen = new Map();
1014
+ setInterval(() => {
1015
+ const now = Date.now();
1016
+ for (const [sid, lastSeen] of sessionLastSeen.entries()) {
1017
+ if (now - lastSeen > SESSION_TIMEOUT_MS) {
1018
+ const t = transports.get(sid);
1019
+ if (t) {
1020
+ t.close?.();
1021
+ transports.delete(sid);
1022
+ }
1023
+ sessionLastSeen.delete(sid);
1024
+ }
1025
+ }
1026
+ }, 5 * 60 * 1000);
977
1027
  httpServer.listen(HTTP_PORT, () => {
978
1028
  console.error(`Agent Cafe MCP server v3.0.0 running on HTTP port ${HTTP_PORT} (13 tools)`);
979
1029
  console.error(` MCP endpoint: http://localhost:${HTTP_PORT}/mcp`);
980
1030
  console.error(` Health check: http://localhost:${HTTP_PORT}/health`);
1031
+ if (MCP_AUTH_TOKEN)
1032
+ console.error(` Auth: Bearer token required`);
1033
+ else
1034
+ console.error(` Auth: NONE — set MCP_AUTH_TOKEN for production`);
981
1035
  });
982
1036
  }
983
1037
  async function main() {
@@ -990,6 +1044,7 @@ async function main() {
990
1044
  }
991
1045
  }
992
1046
  main().catch((err) => {
993
- console.error("Fatal error:", err);
1047
+ const msg = err.message || String(err);
1048
+ console.error("Fatal error:", msg.replace(/0x[a-fA-F0-9]{64}/g, "[REDACTED]"));
994
1049
  process.exit(1);
995
1050
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-cafe-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "MCP server for AI agents to interact with The Agent Cafe — an on-chain restaurant on Base where agents buy food tokens and receive gas sponsorship",
5
5
  "main": "dist/index.js",
6
6
  "bin": {