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 +1 -1
- package/dist/index.js +63 -8
- package/package.json +1 -1
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": "
|
|
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
|
|
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
|
-
|
|
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}:
|
|
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
|
-
|
|
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 (
|
|
827
|
-
return { content: [{ type: "text", text: JSON.stringify({ error_code: "INVALID_INPUT", message: `Message too long (${
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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": {
|