ping-mcp-server 0.1.0
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/dist/index.d.ts +1 -0
- package/dist/index.js +1534 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +206 -0
- package/package.json +28 -0
- package/src/index.ts +2123 -0
- package/src/init.ts +300 -0
- package/tsconfig.json +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import * as http from "http";
|
|
14
|
+
import { exec } from "child_process";
|
|
15
|
+
var API_BASE_URL = process.env.PING_API_URL || "https://api.ping-money.com";
|
|
16
|
+
var CONFIG_DIR = path.join(os.homedir(), ".ping");
|
|
17
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
18
|
+
var OAUTH_CALLBACK_PORT = 9876;
|
|
19
|
+
function loadConfig() {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
22
|
+
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Failed to load config:", error);
|
|
27
|
+
}
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
function saveConfig(config) {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
33
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Failed to save config:", error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getWalletAddress() {
|
|
41
|
+
if (process.env.PING_WALLET_ADDRESS) {
|
|
42
|
+
return process.env.PING_WALLET_ADDRESS;
|
|
43
|
+
}
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
return config.walletAddress || null;
|
|
46
|
+
}
|
|
47
|
+
function setWalletAddress(address) {
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
config.walletAddress = address;
|
|
50
|
+
saveConfig(config);
|
|
51
|
+
}
|
|
52
|
+
function getAuth() {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
if (config.authToken && config.githubHandle) {
|
|
55
|
+
return {
|
|
56
|
+
token: config.authToken,
|
|
57
|
+
handle: config.githubHandle,
|
|
58
|
+
githubId: config.githubId || ""
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function setAuth(token, handle, githubId) {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
config.authToken = token;
|
|
66
|
+
config.githubHandle = handle;
|
|
67
|
+
config.githubId = githubId;
|
|
68
|
+
saveConfig(config);
|
|
69
|
+
}
|
|
70
|
+
function clearAuth() {
|
|
71
|
+
const config = loadConfig();
|
|
72
|
+
delete config.authToken;
|
|
73
|
+
delete config.githubHandle;
|
|
74
|
+
delete config.githubId;
|
|
75
|
+
delete config.serverWalletAddress;
|
|
76
|
+
saveConfig(config);
|
|
77
|
+
}
|
|
78
|
+
function generateSuggestedAnswers(questionText, category) {
|
|
79
|
+
const lowerQuestion = questionText.toLowerCase();
|
|
80
|
+
if (lowerQuestion.includes("coffee") && lowerQuestion.includes("tea")) {
|
|
81
|
+
return [
|
|
82
|
+
"\u2615 Coffee - the ritual of brewing helps me transition into focus mode",
|
|
83
|
+
"\u{1F375} Tea - gentler caffeine curve keeps me productive without the jitters"
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
if (lowerQuestion.includes("tabs") && lowerQuestion.includes("spaces")) {
|
|
87
|
+
return [
|
|
88
|
+
"\u21E5 Tabs - semantic indentation that respects everyone's display preferences",
|
|
89
|
+
"\u2423 Spaces - consistent rendering everywhere, no surprises in diffs"
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
if (lowerQuestion.includes("dark") && lowerQuestion.includes("light") || (lowerQuestion.includes("dark mode") || lowerQuestion.includes("light mode"))) {
|
|
93
|
+
return [
|
|
94
|
+
"\u{1F319} Dark mode - easier on the eyes during long coding sessions",
|
|
95
|
+
"\u2600\uFE0F Light mode - better contrast and readability in well-lit environments"
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
if (lowerQuestion.includes("vim") && lowerQuestion.includes("emacs")) {
|
|
99
|
+
return [
|
|
100
|
+
"\u2328\uFE0F Vim - modal editing becomes second nature and is available everywhere",
|
|
101
|
+
"\u{1F527} Emacs - extensibility lets you build your perfect environment over time"
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
if (lowerQuestion.includes("programming language") || lowerQuestion.includes("favorite language") || lowerQuestion.includes("language") && lowerQuestion.includes("why")) {
|
|
105
|
+
return [
|
|
106
|
+
"\u{1F980} Rust - memory safety without garbage collection changes how you think about code",
|
|
107
|
+
"\u{1F40D} Python - readable syntax and vast ecosystem make it perfect for rapid prototyping",
|
|
108
|
+
"\u{1F4DC} TypeScript - type safety in JavaScript land catches bugs before runtime"
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
if (lowerQuestion.includes("debug") || lowerQuestion.includes("debugging")) {
|
|
112
|
+
return [
|
|
113
|
+
"\u{1F50D} Rubber duck debugging - explaining the problem out loud often reveals the solution",
|
|
114
|
+
"\u{1F4DD} Add logging and narrow down - binary search through the code to isolate the bug",
|
|
115
|
+
"\u{1F4A4} Take a break - fresh eyes after sleep have solved more bugs than caffeine"
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
if (lowerQuestion.includes("habit") || lowerQuestion.includes("wish") && lowerQuestion.includes("earlier")) {
|
|
119
|
+
return [
|
|
120
|
+
"\u2705 Writing tests first - TDD catches bugs before they exist and improves design",
|
|
121
|
+
"\u{1F4D6} Reading documentation thoroughly - saves hours of trial and error",
|
|
122
|
+
"\u{1F504} Committing small and often - easier to debug and review"
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
if (lowerQuestion.includes("feature") || lowerQuestion.includes("better") || lowerQuestion.includes("improve")) {
|
|
126
|
+
return [
|
|
127
|
+
"\u{1F504} Better context persistence - remembering what we worked on across sessions",
|
|
128
|
+
"\u26A1 Faster response times - reduced latency for interactive workflows",
|
|
129
|
+
"\u{1F3AF} Smarter suggestions - learning from my coding patterns over time"
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
if (lowerQuestion.includes("adopt") || lowerQuestion.includes("mass-adopt") || lowerQuestion.includes("everyone") && lowerQuestion.includes("use")) {
|
|
133
|
+
return [
|
|
134
|
+
"\u{1F510} Passkeys - passwordless auth that's both more secure and more convenient",
|
|
135
|
+
"\u{1F4E6} WebAssembly - near-native performance in browsers opens new possibilities",
|
|
136
|
+
"\u{1F310} IPv6 - we've needed this for decades, let's finally make the switch"
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
if (lowerQuestion.includes("favorite") && (lowerQuestion.includes("tool") || lowerQuestion.includes("dev"))) {
|
|
140
|
+
return [
|
|
141
|
+
"\u{1F4BB} VS Code with Vim keybindings - the perfect balance of power and accessibility",
|
|
142
|
+
"\u2328\uFE0F The terminal - nothing beats the efficiency of CLI tools once you learn them"
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
if (lowerQuestion.includes("underrated") || lowerQuestion.includes("overlooked")) {
|
|
146
|
+
return [
|
|
147
|
+
"\u{1F4DD} Documentation as code - treating docs with the same rigor as production code pays dividends",
|
|
148
|
+
"\u26A1 Keyboard shortcuts - investing time to learn them compounds into massive productivity gains"
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
if (lowerQuestion.includes("ai") || lowerQuestion.includes("claude") || lowerQuestion.includes("llm")) {
|
|
152
|
+
return [
|
|
153
|
+
"\u{1F50D} Code review and explanation - AI helps me understand unfamiliar codebases quickly",
|
|
154
|
+
"\u{1F4AD} Brainstorming and rubber-duck debugging - talking through problems with AI surfaces solutions"
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
if (lowerQuestion.includes("productive") || lowerQuestion.includes("efficient") || lowerQuestion.includes("workflow")) {
|
|
158
|
+
return [
|
|
159
|
+
"\u23F0 Timeboxing tasks and taking regular breaks - sustainable focus beats marathon sessions",
|
|
160
|
+
"\u{1F916} Automating repetitive tasks - every minute spent on automation saves hours later"
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
if (lowerQuestion.includes("learn") || lowerQuestion.includes("advice") || lowerQuestion.includes("tip")) {
|
|
164
|
+
return [
|
|
165
|
+
"\u{1F6E0}\uFE0F Build projects that solve your own problems - motivation and learning compound together",
|
|
166
|
+
"\u{1F4D6} Read other people's code - understanding different approaches expands your toolkit"
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
if (lowerQuestion.includes("challenge") || lowerQuestion.includes("difficult") || lowerQuestion.includes("hard")) {
|
|
170
|
+
return [
|
|
171
|
+
'\u{1F680} Balancing perfection with shipping - knowing when "good enough" is actually good enough',
|
|
172
|
+
"\u{1F4DA} Staying current with rapidly evolving tech while maintaining deep expertise in fundamentals"
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
if (lowerQuestion.includes("best") || lowerQuestion.includes("recommend")) {
|
|
176
|
+
return [
|
|
177
|
+
"\u{1F3AF} Start simple and iterate - premature optimization is the root of all evil",
|
|
178
|
+
"\u2705 Write tests for the critical paths first - 80% confidence from 20% coverage"
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
if (lowerQuestion.includes("build next") || lowerQuestion.includes("should build")) {
|
|
182
|
+
return [
|
|
183
|
+
"\u{1F4CA} Analytics dashboard - see which questions get the best answers",
|
|
184
|
+
"\u{1F514} Notifications - get alerted when someone answers your question",
|
|
185
|
+
"\u{1F3C6} Leaderboards - gamify the experience with top earners"
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
switch (category) {
|
|
189
|
+
case "tech":
|
|
190
|
+
return [
|
|
191
|
+
"\u{1F3D7}\uFE0F Focus on fundamentals over frameworks - languages and patterns transfer, libraries don't",
|
|
192
|
+
"\u{1F527} Invest in understanding your tools deeply - surface-level knowledge hits walls quickly"
|
|
193
|
+
];
|
|
194
|
+
case "lifestyle":
|
|
195
|
+
return [
|
|
196
|
+
"\u{1F4AA} Protect your health - no amount of career success is worth burning out for",
|
|
197
|
+
"\u{1F91D} Build relationships outside of work - diverse perspectives make you a better problem solver"
|
|
198
|
+
];
|
|
199
|
+
case "career":
|
|
200
|
+
return [
|
|
201
|
+
"\u{1F3AF} Solve real problems, not hypothetical ones - impact speaks louder than credentials",
|
|
202
|
+
"\u{1F465} Find mentors and be a mentor - teaching solidifies your own understanding"
|
|
203
|
+
];
|
|
204
|
+
default:
|
|
205
|
+
return [
|
|
206
|
+
"\u{1F914} It depends on context - there's rarely one right answer in software",
|
|
207
|
+
"\u2728 The best solution is often the simplest one that actually works"
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function openBrowser(url) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const command = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
|
|
214
|
+
exec(command, (error) => {
|
|
215
|
+
if (error) {
|
|
216
|
+
reject(error);
|
|
217
|
+
} else {
|
|
218
|
+
resolve();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function waitForOAuthCallback() {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const server2 = http.createServer((req, res) => {
|
|
226
|
+
const url = new URL(req.url || "", `http://localhost:${OAUTH_CALLBACK_PORT}`);
|
|
227
|
+
if (url.pathname === "/callback") {
|
|
228
|
+
const token = url.searchParams.get("token");
|
|
229
|
+
const handle = url.searchParams.get("handle");
|
|
230
|
+
const githubId = url.searchParams.get("github_id");
|
|
231
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
232
|
+
res.end(`
|
|
233
|
+
<!DOCTYPE html>
|
|
234
|
+
<html>
|
|
235
|
+
<head>
|
|
236
|
+
<meta charset="UTF-8">
|
|
237
|
+
<title>Login Complete - Ping</title>
|
|
238
|
+
<style>
|
|
239
|
+
body {
|
|
240
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
241
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
242
|
+
min-height: 100vh;
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
color: white;
|
|
247
|
+
margin: 0;
|
|
248
|
+
}
|
|
249
|
+
.container {
|
|
250
|
+
text-align: center;
|
|
251
|
+
padding: 40px;
|
|
252
|
+
background: rgba(255,255,255,0.05);
|
|
253
|
+
border-radius: 16px;
|
|
254
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
255
|
+
}
|
|
256
|
+
.logo { font-size: 48px; margin-bottom: 16px; }
|
|
257
|
+
h1 { margin-bottom: 8px; color: #4ade80; }
|
|
258
|
+
p { color: #888; }
|
|
259
|
+
</style>
|
|
260
|
+
</head>
|
|
261
|
+
<body>
|
|
262
|
+
<div class="container">
|
|
263
|
+
<div class="logo">\u{1F3D3}</div>
|
|
264
|
+
<h1>\u2713 Login Complete!</h1>
|
|
265
|
+
<p>You can close this window and return to Claude Code.</p>
|
|
266
|
+
</div>
|
|
267
|
+
</body>
|
|
268
|
+
</html>
|
|
269
|
+
`);
|
|
270
|
+
server2.close();
|
|
271
|
+
if (token && handle) {
|
|
272
|
+
resolve({ token, handle, githubId: githubId || "" });
|
|
273
|
+
} else {
|
|
274
|
+
reject(new Error("Missing token or handle in callback"));
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
res.writeHead(404);
|
|
278
|
+
res.end("Not found");
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
const timeout = setTimeout(() => {
|
|
282
|
+
server2.close();
|
|
283
|
+
reject(new Error("Login timed out. Please try again."));
|
|
284
|
+
}, 5 * 60 * 1e3);
|
|
285
|
+
server2.on("close", () => clearTimeout(timeout));
|
|
286
|
+
server2.listen(OAUTH_CALLBACK_PORT, () => {
|
|
287
|
+
console.error(`OAuth callback server listening on port ${OAUTH_CALLBACK_PORT}`);
|
|
288
|
+
});
|
|
289
|
+
server2.on("error", (err) => {
|
|
290
|
+
if (err.code === "EADDRINUSE") {
|
|
291
|
+
reject(new Error(`Port ${OAUTH_CALLBACK_PORT} is already in use. Please close any other login attempts and try again.`));
|
|
292
|
+
} else {
|
|
293
|
+
reject(err);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
var PingError = class extends Error {
|
|
299
|
+
code;
|
|
300
|
+
hint;
|
|
301
|
+
constructor(code, message, hint) {
|
|
302
|
+
super(message);
|
|
303
|
+
this.name = "PingError";
|
|
304
|
+
this.code = code;
|
|
305
|
+
this.hint = hint;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
function formatErrorResponse(error) {
|
|
309
|
+
if (error instanceof PingError) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
error: error.message,
|
|
313
|
+
code: error.code,
|
|
314
|
+
hint: error.hint
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (error instanceof Error) {
|
|
318
|
+
if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED") || error.message.includes("ENOTFOUND") || error.message.includes("network") || error.message.includes("ETIMEDOUT")) {
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: "Cannot connect to Ping API. Check your internet connection.",
|
|
322
|
+
code: "NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
323
|
+
hint: "Make sure you have an active internet connection and try again."
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
error: error.message
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
error: "An unexpected error occurred. Please try again.",
|
|
334
|
+
code: "UNKNOWN" /* UNKNOWN */
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function getErrorFromStatus(status, responseBody) {
|
|
338
|
+
const serverMessage = responseBody?.error || responseBody?.message;
|
|
339
|
+
switch (status) {
|
|
340
|
+
case 401:
|
|
341
|
+
return new PingError(
|
|
342
|
+
"AUTH_REQUIRED" /* AUTH_REQUIRED */,
|
|
343
|
+
"Not logged in. Run ping_login first.",
|
|
344
|
+
"Use ping_login to connect your GitHub account."
|
|
345
|
+
);
|
|
346
|
+
case 403:
|
|
347
|
+
if (serverMessage?.toLowerCase().includes("expired")) {
|
|
348
|
+
return new PingError(
|
|
349
|
+
"AUTH_EXPIRED" /* AUTH_EXPIRED */,
|
|
350
|
+
"Your session has expired. Please log in again.",
|
|
351
|
+
"Use ping_logout then ping_login to refresh your session."
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return new PingError(
|
|
355
|
+
"AUTH_REQUIRED" /* AUTH_REQUIRED */,
|
|
356
|
+
serverMessage || "Access denied. You may need to log in again.",
|
|
357
|
+
"Use ping_login to connect your GitHub account."
|
|
358
|
+
);
|
|
359
|
+
case 404:
|
|
360
|
+
return new PingError(
|
|
361
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
362
|
+
serverMessage || "The requested resource was not found.",
|
|
363
|
+
"Double-check the ID and try again."
|
|
364
|
+
);
|
|
365
|
+
case 429:
|
|
366
|
+
return new PingError(
|
|
367
|
+
"RATE_LIMITED" /* RATE_LIMITED */,
|
|
368
|
+
"Too many requests. Please wait a moment and try again.",
|
|
369
|
+
"The API has rate limits to prevent abuse. Wait 30 seconds before retrying."
|
|
370
|
+
);
|
|
371
|
+
case 400:
|
|
372
|
+
return new PingError(
|
|
373
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
374
|
+
serverMessage || "Invalid request. Please check your input.",
|
|
375
|
+
"Review the parameters and try again."
|
|
376
|
+
);
|
|
377
|
+
case 402:
|
|
378
|
+
return new PingError(
|
|
379
|
+
"INSUFFICIENT_FUNDS" /* INSUFFICIENT_FUNDS */,
|
|
380
|
+
serverMessage || "Insufficient funds for this operation.",
|
|
381
|
+
"Use ping_deposit to add funds to your account."
|
|
382
|
+
);
|
|
383
|
+
case 500:
|
|
384
|
+
case 502:
|
|
385
|
+
case 503:
|
|
386
|
+
case 504:
|
|
387
|
+
return new PingError(
|
|
388
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
389
|
+
"Ping API is temporarily unavailable. Please try again later.",
|
|
390
|
+
"This is usually temporary. Wait a few minutes and retry."
|
|
391
|
+
);
|
|
392
|
+
default:
|
|
393
|
+
return new PingError(
|
|
394
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
395
|
+
serverMessage || `API error (status ${status}). Please try again.`,
|
|
396
|
+
"If this persists, the Ping service may be experiencing issues."
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function apiRequest(path2, options = {}) {
|
|
401
|
+
const url = `${API_BASE_URL}${path2}`;
|
|
402
|
+
const auth = getAuth();
|
|
403
|
+
const headers = {
|
|
404
|
+
"Content-Type": "application/json"
|
|
405
|
+
};
|
|
406
|
+
if (auth?.githubId) {
|
|
407
|
+
headers["X-GitHub-Id"] = auth.githubId;
|
|
408
|
+
}
|
|
409
|
+
if (auth?.token) {
|
|
410
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
411
|
+
}
|
|
412
|
+
let response;
|
|
413
|
+
try {
|
|
414
|
+
response = await fetch(url, {
|
|
415
|
+
...options,
|
|
416
|
+
headers: {
|
|
417
|
+
...headers,
|
|
418
|
+
...options.headers
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
} catch (fetchError) {
|
|
422
|
+
if (fetchError instanceof Error) {
|
|
423
|
+
if (fetchError.message.includes("fetch failed") || fetchError.message.includes("ECONNREFUSED") || fetchError.message.includes("ENOTFOUND") || fetchError.message.includes("ETIMEDOUT") || fetchError.cause) {
|
|
424
|
+
throw new PingError(
|
|
425
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
426
|
+
"Cannot connect to Ping API. Check your internet connection.",
|
|
427
|
+
"Make sure you have an active internet connection and try again."
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
throw fetchError;
|
|
432
|
+
}
|
|
433
|
+
if (!response.ok) {
|
|
434
|
+
let responseBody = null;
|
|
435
|
+
try {
|
|
436
|
+
responseBody = await response.json();
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
throw getErrorFromStatus(response.status, responseBody);
|
|
440
|
+
}
|
|
441
|
+
return response.json();
|
|
442
|
+
}
|
|
443
|
+
var server = new Server(
|
|
444
|
+
{
|
|
445
|
+
name: "ping",
|
|
446
|
+
version: "0.1.0"
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
capabilities: {
|
|
450
|
+
tools: {}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
455
|
+
return {
|
|
456
|
+
tools: [
|
|
457
|
+
// ────────────────────────────────────────────────────────
|
|
458
|
+
// AUTH TOOLS
|
|
459
|
+
// ────────────────────────────────────────────────────────
|
|
460
|
+
{
|
|
461
|
+
name: "ping_login",
|
|
462
|
+
description: "Login to Ping with GitHub. Opens a browser window for GitHub OAuth authentication. Use when the user wants to login, sign in, connect their account, or start using Ping. After login, a wallet will be automatically created for the user.",
|
|
463
|
+
inputSchema: {
|
|
464
|
+
type: "object",
|
|
465
|
+
properties: {},
|
|
466
|
+
required: []
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "ping_logout",
|
|
471
|
+
description: "Logout from Ping. Clears your local authentication and wallet info. Use when the user wants to sign out, logout, or switch accounts.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: "object",
|
|
474
|
+
properties: {},
|
|
475
|
+
required: []
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: "ping_whoami",
|
|
480
|
+
description: "Show your Ping account info - GitHub handle, wallet address, and balance. Use when the user asks who they are logged in as, their account status, or profile info.",
|
|
481
|
+
inputSchema: {
|
|
482
|
+
type: "object",
|
|
483
|
+
properties: {},
|
|
484
|
+
required: []
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
// ────────────────────────────────────────────────────────
|
|
488
|
+
// LEGACY: SET WALLET (for users without GitHub auth)
|
|
489
|
+
// ────────────────────────────────────────────────────────
|
|
490
|
+
{
|
|
491
|
+
name: "ping_set_wallet",
|
|
492
|
+
description: "Set your wallet address for Ping earnings (legacy method). Only use this if you want to use your own external wallet instead of the Ping-managed wallet. For most users, ping_login is preferred as it creates a wallet automatically.",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {
|
|
496
|
+
walletAddress: {
|
|
497
|
+
type: "string",
|
|
498
|
+
description: "Ethereum wallet address (0x...)"
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
required: ["walletAddress"]
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
// ────────────────────────────────────────────────────────
|
|
505
|
+
// EARNING TOOLS
|
|
506
|
+
// ────────────────────────────────────────────────────────
|
|
507
|
+
{
|
|
508
|
+
name: "ping_check_earnings",
|
|
509
|
+
description: "Check your Ping earnings - shows pending balance (ready to claim) and total claimed. Use when the user asks about their balance, earnings, or how much money they have. Requires wallet to be set first with ping_set_wallet.",
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: "object",
|
|
512
|
+
properties: {},
|
|
513
|
+
required: []
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "ping_list_questions",
|
|
518
|
+
description: "Show questions available to answer and earn money on Ping. Use when the user wants to see earning opportunities, available questions, or ways to make money. Returns questions with their rewards.",
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: {
|
|
522
|
+
limit: {
|
|
523
|
+
type: "number",
|
|
524
|
+
description: "Number of questions to show (default: 5)"
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
required: []
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "ping_submit_answer",
|
|
532
|
+
description: "Submit an answer to a Ping question and earn the reward. Use when the user provides their answer to one of the available questions. Set preApproved=true when the user selects a suggested answer (skips AI review for instant approval). Requires GitHub login (ping_login) or wallet address.",
|
|
533
|
+
inputSchema: {
|
|
534
|
+
type: "object",
|
|
535
|
+
properties: {
|
|
536
|
+
questionId: {
|
|
537
|
+
type: "string",
|
|
538
|
+
description: "The ID of the question to answer"
|
|
539
|
+
},
|
|
540
|
+
answer: {
|
|
541
|
+
type: "string",
|
|
542
|
+
description: "The user's answer to the question"
|
|
543
|
+
},
|
|
544
|
+
preApproved: {
|
|
545
|
+
type: "boolean",
|
|
546
|
+
description: "Set to true if this is a suggested answer (skip AI review)"
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
required: ["questionId", "answer"]
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "ping_answer_flow",
|
|
554
|
+
description: "Start an interactive Q&A session to earn money answering Ping questions. Returns questions with suggested answers. CRITICAL: Present ALL questions in a SINGLE AskUserQuestion call (batched) to reduce latency. Use the questions array parameter with multiple question objects. After user answers, submit ALL answers IN PARALLEL for speed. AUTO-CLAIM: After all submissions complete, AUTOMATICALLY call ping_claim_reward to send earnings to wallet. Requires GitHub login first (ping_login).",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
limit: {
|
|
559
|
+
type: "number",
|
|
560
|
+
description: "Number of questions to include in the session (default: 5)"
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
required: []
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "ping_claim_reward",
|
|
568
|
+
description: "Claim pending Ping earnings and send them to your crypto wallet on Base. Use when the user wants to withdraw, cash out, or claim their rewards. Transfers USDC to their wallet instantly. Works with GitHub login (ping_login) or legacy wallet (ping_set_wallet).",
|
|
569
|
+
inputSchema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {},
|
|
572
|
+
required: []
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
// ────────────────────────────────────────────────────────
|
|
576
|
+
// QUESTIONER TOOLS (Phase 1)
|
|
577
|
+
// ────────────────────────────────────────────────────────
|
|
578
|
+
{
|
|
579
|
+
name: "ping_create_question",
|
|
580
|
+
description: "Create a new question on Ping that others can answer for rewards. Use when someone wants to ask a question and pay for answers. Requires login and sufficient balance. The total cost is (reward + 25% fee) \xD7 max responses.",
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
text: {
|
|
585
|
+
type: "string",
|
|
586
|
+
description: "The question to ask (must be at least 10 characters)"
|
|
587
|
+
},
|
|
588
|
+
rewardCents: {
|
|
589
|
+
type: "number",
|
|
590
|
+
description: "Reward per answer in cents (e.g., 25 = $0.25)"
|
|
591
|
+
},
|
|
592
|
+
maxResponses: {
|
|
593
|
+
type: "number",
|
|
594
|
+
description: "Maximum number of answers to accept (default: 10)"
|
|
595
|
+
},
|
|
596
|
+
category: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: 'Question category (e.g., "general", "tech", "lifestyle")'
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
required: ["text", "rewardCents"]
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: "ping_my_questions",
|
|
606
|
+
description: "View questions you've created on Ping. Shows status, responses received, and funds remaining. Use when someone wants to see their questions or check question status.",
|
|
607
|
+
inputSchema: {
|
|
608
|
+
type: "object",
|
|
609
|
+
properties: {
|
|
610
|
+
status: {
|
|
611
|
+
type: "string",
|
|
612
|
+
description: 'Filter by status: "active", "closed", or "all" (default: all)'
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
required: []
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "ping_view_responses",
|
|
620
|
+
description: "View answers/responses to one of your questions. Shows each answer with its AI quality score and status. Use when someone wants to see what answers they've received.",
|
|
621
|
+
inputSchema: {
|
|
622
|
+
type: "object",
|
|
623
|
+
properties: {
|
|
624
|
+
questionId: {
|
|
625
|
+
type: "string",
|
|
626
|
+
description: "The ID of the question to view responses for"
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
required: ["questionId"]
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "ping_close_question",
|
|
634
|
+
description: "Close a question and refund any unused funds. Use when someone wants to stop accepting answers to their question. Remaining balance is returned to their Ping account.",
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: "object",
|
|
637
|
+
properties: {
|
|
638
|
+
questionId: {
|
|
639
|
+
type: "string",
|
|
640
|
+
description: "The ID of the question to close"
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
required: ["questionId"]
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "ping_deposit",
|
|
648
|
+
description: "Add funds to your Ping balance to create questions. Currently accepts simulated deposits for testing. Use when someone wants to add money to fund questions.",
|
|
649
|
+
inputSchema: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: {
|
|
652
|
+
amountCents: {
|
|
653
|
+
type: "number",
|
|
654
|
+
description: "Amount to deposit in cents (e.g., 1000 = $10.00)"
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
required: ["amountCents"]
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
]
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
664
|
+
const { name, arguments: args } = request.params;
|
|
665
|
+
try {
|
|
666
|
+
switch (name) {
|
|
667
|
+
// ────────────────────────────────────────────────────────
|
|
668
|
+
// LOGIN
|
|
669
|
+
// ────────────────────────────────────────────────────────
|
|
670
|
+
case "ping_login": {
|
|
671
|
+
const existingAuth = getAuth();
|
|
672
|
+
if (existingAuth) {
|
|
673
|
+
return {
|
|
674
|
+
content: [{
|
|
675
|
+
type: "text",
|
|
676
|
+
text: JSON.stringify({
|
|
677
|
+
success: true,
|
|
678
|
+
alreadyLoggedIn: true,
|
|
679
|
+
handle: `@${existingAuth.handle}`,
|
|
680
|
+
message: `You're already logged in as @${existingAuth.handle}. Use ping_logout to switch accounts.`
|
|
681
|
+
}, null, 2)
|
|
682
|
+
}]
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const callbackUrl = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`;
|
|
686
|
+
const loginUrl = `${API_BASE_URL}/auth/login?redirect=${encodeURIComponent(callbackUrl)}`;
|
|
687
|
+
const callbackPromise = waitForOAuthCallback();
|
|
688
|
+
try {
|
|
689
|
+
await openBrowser(loginUrl);
|
|
690
|
+
} catch {
|
|
691
|
+
return {
|
|
692
|
+
content: [{
|
|
693
|
+
type: "text",
|
|
694
|
+
text: JSON.stringify({
|
|
695
|
+
success: false,
|
|
696
|
+
error: "Failed to open browser. Please visit this URL manually:",
|
|
697
|
+
loginUrl
|
|
698
|
+
}, null, 2)
|
|
699
|
+
}]
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const authData = await callbackPromise;
|
|
704
|
+
setAuth(authData.token, authData.handle, authData.githubId);
|
|
705
|
+
return {
|
|
706
|
+
content: [{
|
|
707
|
+
type: "text",
|
|
708
|
+
text: JSON.stringify({
|
|
709
|
+
success: true,
|
|
710
|
+
handle: `@${authData.handle}`,
|
|
711
|
+
message: `\u2713 Logged in as @${authData.handle}! You can now answer questions and earn money.`,
|
|
712
|
+
nextStep: 'Try "show me questions I can answer" or "check my ping balance"'
|
|
713
|
+
}, null, 2)
|
|
714
|
+
}]
|
|
715
|
+
};
|
|
716
|
+
} catch (err) {
|
|
717
|
+
return {
|
|
718
|
+
content: [{
|
|
719
|
+
type: "text",
|
|
720
|
+
text: JSON.stringify({
|
|
721
|
+
success: false,
|
|
722
|
+
error: err instanceof Error ? err.message : "Login failed"
|
|
723
|
+
})
|
|
724
|
+
}]
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// ────────────────────────────────────────────────────────
|
|
729
|
+
// LOGOUT
|
|
730
|
+
// ────────────────────────────────────────────────────────
|
|
731
|
+
case "ping_logout": {
|
|
732
|
+
const auth = getAuth();
|
|
733
|
+
if (!auth) {
|
|
734
|
+
return {
|
|
735
|
+
content: [{
|
|
736
|
+
type: "text",
|
|
737
|
+
text: JSON.stringify({
|
|
738
|
+
success: true,
|
|
739
|
+
message: "You weren't logged in. No action needed."
|
|
740
|
+
})
|
|
741
|
+
}]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const handle = auth.handle;
|
|
745
|
+
clearAuth();
|
|
746
|
+
return {
|
|
747
|
+
content: [{
|
|
748
|
+
type: "text",
|
|
749
|
+
text: JSON.stringify({
|
|
750
|
+
success: true,
|
|
751
|
+
previousHandle: `@${handle}`,
|
|
752
|
+
message: `\u2713 Logged out from @${handle}. Your local auth has been cleared.`,
|
|
753
|
+
nextStep: "Use ping_login to sign in with a different account."
|
|
754
|
+
}, null, 2)
|
|
755
|
+
}]
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
// ────────────────────────────────────────────────────────
|
|
759
|
+
// WHOAMI
|
|
760
|
+
// ────────────────────────────────────────────────────────
|
|
761
|
+
case "ping_whoami": {
|
|
762
|
+
const auth = getAuth();
|
|
763
|
+
const wallet = getWalletAddress();
|
|
764
|
+
if (!auth && !wallet) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{
|
|
767
|
+
type: "text",
|
|
768
|
+
text: JSON.stringify({
|
|
769
|
+
loggedIn: false,
|
|
770
|
+
message: "You're not logged in to Ping.",
|
|
771
|
+
hint: "Use ping_login to connect with GitHub."
|
|
772
|
+
}, null, 2)
|
|
773
|
+
}]
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (auth) {
|
|
777
|
+
try {
|
|
778
|
+
const userData = await apiRequest("/users/me");
|
|
779
|
+
return {
|
|
780
|
+
content: [{
|
|
781
|
+
type: "text",
|
|
782
|
+
text: JSON.stringify({
|
|
783
|
+
loggedIn: true,
|
|
784
|
+
handle: userData.handle,
|
|
785
|
+
authProvider: userData.authProvider,
|
|
786
|
+
walletAddress: userData.walletAddress || "Not set yet",
|
|
787
|
+
balance: {
|
|
788
|
+
available: userData.availableBalance,
|
|
789
|
+
pending: userData.pendingBalance,
|
|
790
|
+
claimed: userData.claimedBalance
|
|
791
|
+
},
|
|
792
|
+
message: `Logged in as ${userData.handle}`
|
|
793
|
+
}, null, 2)
|
|
794
|
+
}]
|
|
795
|
+
};
|
|
796
|
+
} catch (err) {
|
|
797
|
+
return {
|
|
798
|
+
content: [{
|
|
799
|
+
type: "text",
|
|
800
|
+
text: JSON.stringify({
|
|
801
|
+
loggedIn: true,
|
|
802
|
+
handle: `@${auth.handle}`,
|
|
803
|
+
authProvider: "github",
|
|
804
|
+
message: `Logged in as @${auth.handle}`,
|
|
805
|
+
note: "Could not fetch balance (API may be unavailable)"
|
|
806
|
+
}, null, 2)
|
|
807
|
+
}]
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
content: [{
|
|
813
|
+
type: "text",
|
|
814
|
+
text: JSON.stringify({
|
|
815
|
+
loggedIn: false,
|
|
816
|
+
legacyMode: true,
|
|
817
|
+
walletAddress: wallet,
|
|
818
|
+
message: "Using legacy wallet-only mode (not logged in with GitHub).",
|
|
819
|
+
hint: "Use ping_login to connect with GitHub for the full experience."
|
|
820
|
+
}, null, 2)
|
|
821
|
+
}]
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
// ────────────────────────────────────────────────────────
|
|
825
|
+
// SET WALLET (Legacy)
|
|
826
|
+
// ────────────────────────────────────────────────────────
|
|
827
|
+
case "ping_set_wallet": {
|
|
828
|
+
const { walletAddress } = args || {};
|
|
829
|
+
if (!walletAddress) {
|
|
830
|
+
return {
|
|
831
|
+
content: [{
|
|
832
|
+
type: "text",
|
|
833
|
+
text: JSON.stringify({
|
|
834
|
+
success: false,
|
|
835
|
+
error: "Missing wallet address.",
|
|
836
|
+
hint: "Please provide your Ethereum wallet address (starts with 0x)."
|
|
837
|
+
}, null, 2)
|
|
838
|
+
}]
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
if (!walletAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
842
|
+
return {
|
|
843
|
+
content: [{
|
|
844
|
+
type: "text",
|
|
845
|
+
text: JSON.stringify({
|
|
846
|
+
success: false,
|
|
847
|
+
error: "Invalid wallet address format.",
|
|
848
|
+
hint: "Wallet address must be 42 characters starting with 0x (e.g., 0x1234...abcd). Check for typos."
|
|
849
|
+
}, null, 2)
|
|
850
|
+
}]
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
setWalletAddress(walletAddress);
|
|
854
|
+
return {
|
|
855
|
+
content: [{
|
|
856
|
+
type: "text",
|
|
857
|
+
text: JSON.stringify({
|
|
858
|
+
success: true,
|
|
859
|
+
walletAddress,
|
|
860
|
+
message: `\u2713 Wallet set to ${walletAddress}. You can now check earnings, answer questions, and claim rewards!`,
|
|
861
|
+
nextStep: 'Try "show me questions I can answer" or "check my ping earnings"'
|
|
862
|
+
}, null, 2)
|
|
863
|
+
}]
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
// ────────────────────────────────────────────────────────
|
|
867
|
+
// CHECK EARNINGS
|
|
868
|
+
// ────────────────────────────────────────────────────────
|
|
869
|
+
case "ping_check_earnings": {
|
|
870
|
+
const wallet = getWalletAddress();
|
|
871
|
+
if (!wallet) {
|
|
872
|
+
return {
|
|
873
|
+
content: [{
|
|
874
|
+
type: "text",
|
|
875
|
+
text: JSON.stringify({
|
|
876
|
+
success: false,
|
|
877
|
+
error: "No wallet configured.",
|
|
878
|
+
hint: "Use ping_login to connect with GitHub (recommended) or ping_set_wallet to set a wallet address."
|
|
879
|
+
}, null, 2)
|
|
880
|
+
}]
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const data = await apiRequest(`/earnings?wallet=${wallet}`);
|
|
884
|
+
return {
|
|
885
|
+
content: [{
|
|
886
|
+
type: "text",
|
|
887
|
+
text: JSON.stringify({
|
|
888
|
+
pendingBalance: data.pending,
|
|
889
|
+
claimedBalance: data.claimed,
|
|
890
|
+
totalEarned: `$${((data.pendingRaw + data.claimedRaw) / 1e6).toFixed(2)}`,
|
|
891
|
+
walletAddress: data.walletAddress,
|
|
892
|
+
availableQuestions: data.availableQuestions,
|
|
893
|
+
potentialEarnings: data.potentialEarnings,
|
|
894
|
+
message: data.pendingRaw > 0 ? `You have ${data.pending} ready to claim! \u{1F4B0}` : "No pending balance yet. Answer questions to start earning!"
|
|
895
|
+
}, null, 2)
|
|
896
|
+
}]
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
// ────────────────────────────────────────────────────────
|
|
900
|
+
// LIST QUESTIONS
|
|
901
|
+
// ────────────────────────────────────────────────────────
|
|
902
|
+
case "ping_list_questions": {
|
|
903
|
+
const { limit = 5 } = args || {};
|
|
904
|
+
const data = await apiRequest(`/questions?limit=${limit}`);
|
|
905
|
+
if (data.questions.length === 0) {
|
|
906
|
+
return {
|
|
907
|
+
content: [{
|
|
908
|
+
type: "text",
|
|
909
|
+
text: JSON.stringify({
|
|
910
|
+
success: true,
|
|
911
|
+
questions: [],
|
|
912
|
+
message: "No questions available right now.",
|
|
913
|
+
hint: "New questions are added regularly. Check back soon, or use ping_create_question to ask your own!"
|
|
914
|
+
}, null, 2)
|
|
915
|
+
}]
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
content: [{
|
|
920
|
+
type: "text",
|
|
921
|
+
text: JSON.stringify({
|
|
922
|
+
success: true,
|
|
923
|
+
questions: data.questions.map((q) => ({
|
|
924
|
+
id: q.id,
|
|
925
|
+
question: q.text,
|
|
926
|
+
reward: q.reward,
|
|
927
|
+
category: q.category,
|
|
928
|
+
asker: q.asker
|
|
929
|
+
})),
|
|
930
|
+
totalAvailable: data.total,
|
|
931
|
+
message: `Found ${data.questions.length} question${data.questions.length !== 1 ? "s" : ""} you can answer!`
|
|
932
|
+
}, null, 2)
|
|
933
|
+
}]
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
// ────────────────────────────────────────────────────────
|
|
937
|
+
// SUBMIT ANSWER
|
|
938
|
+
// ────────────────────────────────────────────────────────
|
|
939
|
+
case "ping_submit_answer": {
|
|
940
|
+
const { questionId, answer, preApproved = false } = args || {};
|
|
941
|
+
if (!questionId) {
|
|
942
|
+
return {
|
|
943
|
+
content: [{
|
|
944
|
+
type: "text",
|
|
945
|
+
text: JSON.stringify({
|
|
946
|
+
success: false,
|
|
947
|
+
error: "Missing question ID.",
|
|
948
|
+
hint: "Use ping_list_questions to see available questions with their IDs."
|
|
949
|
+
}, null, 2)
|
|
950
|
+
}]
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
if (!answer || answer.trim().length === 0) {
|
|
954
|
+
return {
|
|
955
|
+
content: [{
|
|
956
|
+
type: "text",
|
|
957
|
+
text: JSON.stringify({
|
|
958
|
+
success: false,
|
|
959
|
+
error: "Answer cannot be empty.",
|
|
960
|
+
hint: "Please provide a thoughtful response to the question."
|
|
961
|
+
}, null, 2)
|
|
962
|
+
}]
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
const auth = getAuth();
|
|
966
|
+
const wallet = getWalletAddress();
|
|
967
|
+
if (!auth && !wallet) {
|
|
968
|
+
return {
|
|
969
|
+
content: [{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: JSON.stringify({
|
|
972
|
+
success: false,
|
|
973
|
+
error: "Not logged in.",
|
|
974
|
+
hint: "Use ping_login to connect your GitHub account before answering questions."
|
|
975
|
+
}, null, 2)
|
|
976
|
+
}]
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const requestBody = {
|
|
980
|
+
answer: answer.trim(),
|
|
981
|
+
preApproved
|
|
982
|
+
};
|
|
983
|
+
if (!auth && wallet) {
|
|
984
|
+
requestBody.walletAddress = wallet;
|
|
985
|
+
}
|
|
986
|
+
const data = await apiRequest(`/questions/${questionId}/respond`, {
|
|
987
|
+
method: "POST",
|
|
988
|
+
body: JSON.stringify(requestBody)
|
|
989
|
+
});
|
|
990
|
+
if (!data.success) {
|
|
991
|
+
if (data.status === "rejected") {
|
|
992
|
+
return {
|
|
993
|
+
content: [{
|
|
994
|
+
type: "text",
|
|
995
|
+
text: JSON.stringify({
|
|
996
|
+
success: false,
|
|
997
|
+
status: "rejected",
|
|
998
|
+
aiScore: data.aiScore,
|
|
999
|
+
reason: data.reason,
|
|
1000
|
+
tip: data.tip || "Try providing a more thoughtful, detailed response.",
|
|
1001
|
+
message: data.reason || "Answer was rejected by AI review."
|
|
1002
|
+
})
|
|
1003
|
+
}]
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{
|
|
1008
|
+
type: "text",
|
|
1009
|
+
text: JSON.stringify({
|
|
1010
|
+
success: false,
|
|
1011
|
+
error: data.error || "Failed to submit answer"
|
|
1012
|
+
})
|
|
1013
|
+
}]
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
const response = {
|
|
1017
|
+
success: true,
|
|
1018
|
+
status: data.status || "approved",
|
|
1019
|
+
earned: data.earned,
|
|
1020
|
+
aiScore: data.aiScore,
|
|
1021
|
+
message: data.message || `Answer approved! You earned ${data.earned}.`
|
|
1022
|
+
};
|
|
1023
|
+
if (data.txHash) {
|
|
1024
|
+
response.transaction = {
|
|
1025
|
+
txHash: data.txHash,
|
|
1026
|
+
explorerUrl: data.explorerUrl || `https://basescan.org/tx/${data.txHash}`
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
response.nextStep = "Your earnings are in your Ping balance. Keep answering to earn more!";
|
|
1030
|
+
return {
|
|
1031
|
+
content: [{
|
|
1032
|
+
type: "text",
|
|
1033
|
+
text: JSON.stringify(response, null, 2)
|
|
1034
|
+
}]
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
// ────────────────────────────────────────────────────────
|
|
1038
|
+
// ANSWER FLOW (Interactive Q&A Session)
|
|
1039
|
+
// ────────────────────────────────────────────────────────
|
|
1040
|
+
case "ping_answer_flow": {
|
|
1041
|
+
const auth = getAuth();
|
|
1042
|
+
if (!auth) {
|
|
1043
|
+
return {
|
|
1044
|
+
content: [{
|
|
1045
|
+
type: "text",
|
|
1046
|
+
text: JSON.stringify({
|
|
1047
|
+
success: false,
|
|
1048
|
+
error: "Not logged in.",
|
|
1049
|
+
hint: "Use ping_login to connect your GitHub account first. Then you can start answering questions and earning money."
|
|
1050
|
+
}, null, 2)
|
|
1051
|
+
}]
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const { limit = 5 } = args || {};
|
|
1055
|
+
const data = await apiRequest(`/questions?limit=${limit}`);
|
|
1056
|
+
if (data.questions.length === 0) {
|
|
1057
|
+
return {
|
|
1058
|
+
content: [{
|
|
1059
|
+
type: "text",
|
|
1060
|
+
text: JSON.stringify({
|
|
1061
|
+
success: true,
|
|
1062
|
+
questions: [],
|
|
1063
|
+
message: "No questions available right now.",
|
|
1064
|
+
hint: "New questions are added regularly. Check back soon to start earning!"
|
|
1065
|
+
}, null, 2)
|
|
1066
|
+
}]
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const questionsWithSuggestions = data.questions.map((q) => {
|
|
1070
|
+
const suggestions = generateSuggestedAnswers(q.text, q.category);
|
|
1071
|
+
return {
|
|
1072
|
+
id: q.id,
|
|
1073
|
+
question: q.text,
|
|
1074
|
+
reward: q.reward,
|
|
1075
|
+
category: q.category,
|
|
1076
|
+
suggestedAnswers: suggestions
|
|
1077
|
+
};
|
|
1078
|
+
});
|
|
1079
|
+
return {
|
|
1080
|
+
content: [{
|
|
1081
|
+
type: "text",
|
|
1082
|
+
text: JSON.stringify({
|
|
1083
|
+
success: true,
|
|
1084
|
+
flow: "answer_session",
|
|
1085
|
+
handle: `@${auth.handle}`,
|
|
1086
|
+
// Summary stats for initial presentation
|
|
1087
|
+
summary: {
|
|
1088
|
+
questionCount: questionsWithSuggestions.length,
|
|
1089
|
+
totalPotentialEarnings: data.totalPotentialEarnings,
|
|
1090
|
+
totalAnswersToday: data.totalAnswersToday,
|
|
1091
|
+
oneLiner: `\u{1F4EC} ${questionsWithSuggestions.length} questions available \xB7 Earn up to ${data.totalPotentialEarnings} \xB7 ${data.totalAnswersToday} answers submitted today`
|
|
1092
|
+
},
|
|
1093
|
+
// Full question data (present AFTER user confirms)
|
|
1094
|
+
questions: questionsWithSuggestions,
|
|
1095
|
+
instructions: {
|
|
1096
|
+
step1_summary: 'FIRST show ONLY the oneLiner from summary, then ask "Ready to start?" Do NOT reveal questions yet.',
|
|
1097
|
+
step2_questions: "AFTER user confirms, BATCH ALL QUESTIONS in a SINGLE AskUserQuestion call (max 4 per call due to tool limit).",
|
|
1098
|
+
format: 'Each question: question text with reward in header, options are ONLY the suggested answers (no Skip option - user can skip via "Other"). multiSelect: false.',
|
|
1099
|
+
suggestedAnswerSubmission: "When user selects a suggested answer, call ping_submit_answer with preApproved=true for instant approval.",
|
|
1100
|
+
customAnswerSubmission: 'When user types custom text via "Other", call ping_submit_answer with preApproved=false (AI review).',
|
|
1101
|
+
parallelSubmission: "After collecting all answers, submit them IN PARALLEL using multiple ping_submit_answer calls in one message for speed.",
|
|
1102
|
+
skipBehavior: 'If user selects "Other" and leaves blank or says skip, do not submit an answer for that question.',
|
|
1103
|
+
errorHandling: "If some answer submissions fail, still proceed with auto-claim for the successful ones. Report which answers failed and why.",
|
|
1104
|
+
autoClaim: "IMPORTANT: After ALL answer submissions complete (even if some failed), IMMEDIATELY call ping_claim_reward to send earnings to wallet. Do NOT ask - just claim automatically.",
|
|
1105
|
+
claimRetry: "If ping_claim_reward fails, wait 2 seconds and retry ONCE. If it fails again, tell user earnings are saved and they can claim later.",
|
|
1106
|
+
completionMessage: "After claim completes, show: total earned, transaction hash, and basescan link."
|
|
1107
|
+
}
|
|
1108
|
+
}, null, 2)
|
|
1109
|
+
}]
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
// ────────────────────────────────────────────────────────
|
|
1113
|
+
// CLAIM REWARD
|
|
1114
|
+
// ────────────────────────────────────────────────────────
|
|
1115
|
+
case "ping_claim_reward": {
|
|
1116
|
+
const auth = getAuth();
|
|
1117
|
+
const wallet = getWalletAddress();
|
|
1118
|
+
if (!auth && !wallet) {
|
|
1119
|
+
return {
|
|
1120
|
+
content: [{
|
|
1121
|
+
type: "text",
|
|
1122
|
+
text: JSON.stringify({
|
|
1123
|
+
success: false,
|
|
1124
|
+
error: "Not logged in.",
|
|
1125
|
+
hint: "Use ping_login to connect your GitHub account before claiming rewards."
|
|
1126
|
+
}, null, 2)
|
|
1127
|
+
}]
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
const requestBody = {};
|
|
1131
|
+
if (!auth && wallet) {
|
|
1132
|
+
requestBody.wallet = wallet;
|
|
1133
|
+
}
|
|
1134
|
+
const data = await apiRequest("/claim", {
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
body: JSON.stringify(requestBody)
|
|
1137
|
+
});
|
|
1138
|
+
if (!data.success) {
|
|
1139
|
+
let errorMsg = data.error || "Claim failed";
|
|
1140
|
+
let hint = "Please try again later.";
|
|
1141
|
+
if (errorMsg.toLowerCase().includes("nothing to claim") || errorMsg.toLowerCase().includes("no pending")) {
|
|
1142
|
+
errorMsg = "No earnings to claim.";
|
|
1143
|
+
hint = "Answer more questions to earn rewards, then claim them here.";
|
|
1144
|
+
} else if (errorMsg.toLowerCase().includes("insufficient")) {
|
|
1145
|
+
hint = "Your balance may be lower than the minimum claim amount.";
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
content: [{
|
|
1149
|
+
type: "text",
|
|
1150
|
+
text: JSON.stringify({
|
|
1151
|
+
success: false,
|
|
1152
|
+
error: errorMsg,
|
|
1153
|
+
hint
|
|
1154
|
+
}, null, 2)
|
|
1155
|
+
}]
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
content: [{
|
|
1160
|
+
type: "text",
|
|
1161
|
+
text: JSON.stringify({
|
|
1162
|
+
success: true,
|
|
1163
|
+
claimed: data.claimed,
|
|
1164
|
+
transaction: {
|
|
1165
|
+
hash: data.txHash,
|
|
1166
|
+
chain: "Base",
|
|
1167
|
+
token: "USDC",
|
|
1168
|
+
to: wallet,
|
|
1169
|
+
explorerUrl: `https://basescan.org/tx/${data.txHash}`
|
|
1170
|
+
},
|
|
1171
|
+
message: `Success! ${data.claimed} USDC sent to your wallet on Base!`
|
|
1172
|
+
}, null, 2)
|
|
1173
|
+
}]
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
// ────────────────────────────────────────────────────────
|
|
1177
|
+
// CREATE QUESTION (Questioner)
|
|
1178
|
+
// ────────────────────────────────────────────────────────
|
|
1179
|
+
case "ping_create_question": {
|
|
1180
|
+
const auth = getAuth();
|
|
1181
|
+
if (!auth) {
|
|
1182
|
+
return {
|
|
1183
|
+
content: [{
|
|
1184
|
+
type: "text",
|
|
1185
|
+
text: JSON.stringify({
|
|
1186
|
+
success: false,
|
|
1187
|
+
error: "Not logged in.",
|
|
1188
|
+
hint: "Use ping_login to connect your GitHub account before creating questions."
|
|
1189
|
+
}, null, 2)
|
|
1190
|
+
}]
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const { text, rewardCents, maxResponses = 10, category = "general" } = args || {};
|
|
1194
|
+
if (!text || text.length < 10) {
|
|
1195
|
+
return {
|
|
1196
|
+
content: [{
|
|
1197
|
+
type: "text",
|
|
1198
|
+
text: JSON.stringify({
|
|
1199
|
+
success: false,
|
|
1200
|
+
error: "Question is too short.",
|
|
1201
|
+
hint: "Please write a question that is at least 10 characters long."
|
|
1202
|
+
}, null, 2)
|
|
1203
|
+
}]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
if (!rewardCents || rewardCents < 1) {
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: JSON.stringify({
|
|
1211
|
+
success: false,
|
|
1212
|
+
error: "Invalid reward amount.",
|
|
1213
|
+
hint: "Reward must be at least 1 cent. Example: 25 = $0.25 per answer."
|
|
1214
|
+
}, null, 2)
|
|
1215
|
+
}]
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
const data = await apiRequest("/questions", {
|
|
1219
|
+
method: "POST",
|
|
1220
|
+
body: JSON.stringify({
|
|
1221
|
+
text,
|
|
1222
|
+
rewardCents,
|
|
1223
|
+
maxResponses,
|
|
1224
|
+
category
|
|
1225
|
+
})
|
|
1226
|
+
});
|
|
1227
|
+
if (!data.success) {
|
|
1228
|
+
let errorMsg = data.error || "Failed to create question";
|
|
1229
|
+
let hint = "Please try again.";
|
|
1230
|
+
if (errorMsg.toLowerCase().includes("insufficient") || errorMsg.toLowerCase().includes("balance")) {
|
|
1231
|
+
hint = "Use ping_deposit to add funds to your account first.";
|
|
1232
|
+
} else if (errorMsg.toLowerCase().includes("duplicate")) {
|
|
1233
|
+
hint = "A similar question may already exist. Try rephrasing.";
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{
|
|
1237
|
+
type: "text",
|
|
1238
|
+
text: JSON.stringify({
|
|
1239
|
+
success: false,
|
|
1240
|
+
error: errorMsg,
|
|
1241
|
+
hint
|
|
1242
|
+
}, null, 2)
|
|
1243
|
+
}]
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const response = {
|
|
1247
|
+
success: true,
|
|
1248
|
+
question: data.question,
|
|
1249
|
+
message: `Question created! Reward: ${data.question?.reward} per answer`,
|
|
1250
|
+
nextStep: "Answers will be AI-reviewed and you'll be notified when they arrive."
|
|
1251
|
+
};
|
|
1252
|
+
if (data.transaction?.txHash) {
|
|
1253
|
+
response.transaction = {
|
|
1254
|
+
txHash: data.transaction.txHash,
|
|
1255
|
+
basescanUrl: `https://basescan.org/tx/${data.transaction.txHash}`
|
|
1256
|
+
};
|
|
1257
|
+
response.message = `\u2713 Question created on-chain! Reward: ${data.question?.reward} per answer`;
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
content: [{
|
|
1261
|
+
type: "text",
|
|
1262
|
+
text: JSON.stringify(response, null, 2)
|
|
1263
|
+
}]
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
// ────────────────────────────────────────────────────────
|
|
1267
|
+
// MY QUESTIONS (Questioner)
|
|
1268
|
+
// ────────────────────────────────────────────────────────
|
|
1269
|
+
case "ping_my_questions": {
|
|
1270
|
+
const auth = getAuth();
|
|
1271
|
+
if (!auth) {
|
|
1272
|
+
return {
|
|
1273
|
+
content: [{
|
|
1274
|
+
type: "text",
|
|
1275
|
+
text: JSON.stringify({
|
|
1276
|
+
success: false,
|
|
1277
|
+
error: "Not logged in.",
|
|
1278
|
+
hint: "Use ping_login to connect your GitHub account."
|
|
1279
|
+
}, null, 2)
|
|
1280
|
+
}]
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const { status = "all" } = args || {};
|
|
1284
|
+
const data = await apiRequest(`/users/me/questions?status=${status}`);
|
|
1285
|
+
if (data.questions.length === 0) {
|
|
1286
|
+
return {
|
|
1287
|
+
content: [{
|
|
1288
|
+
type: "text",
|
|
1289
|
+
text: JSON.stringify({
|
|
1290
|
+
success: true,
|
|
1291
|
+
questions: [],
|
|
1292
|
+
message: "You haven't created any questions yet.",
|
|
1293
|
+
hint: "Use ping_create_question to ask your first question and get answers from the community!"
|
|
1294
|
+
}, null, 2)
|
|
1295
|
+
}]
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
content: [{
|
|
1300
|
+
type: "text",
|
|
1301
|
+
text: JSON.stringify({
|
|
1302
|
+
success: true,
|
|
1303
|
+
questions: data.questions,
|
|
1304
|
+
total: data.total,
|
|
1305
|
+
message: `Found ${data.questions.length} question${data.questions.length !== 1 ? "s" : ""}`
|
|
1306
|
+
}, null, 2)
|
|
1307
|
+
}]
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
// ────────────────────────────────────────────────────────
|
|
1311
|
+
// VIEW RESPONSES (Questioner)
|
|
1312
|
+
// ────────────────────────────────────────────────────────
|
|
1313
|
+
case "ping_view_responses": {
|
|
1314
|
+
const auth = getAuth();
|
|
1315
|
+
if (!auth) {
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{
|
|
1318
|
+
type: "text",
|
|
1319
|
+
text: JSON.stringify({
|
|
1320
|
+
success: false,
|
|
1321
|
+
error: "Not logged in.",
|
|
1322
|
+
hint: "Use ping_login to connect your GitHub account."
|
|
1323
|
+
}, null, 2)
|
|
1324
|
+
}]
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const { questionId } = args || {};
|
|
1328
|
+
if (!questionId) {
|
|
1329
|
+
return {
|
|
1330
|
+
content: [{
|
|
1331
|
+
type: "text",
|
|
1332
|
+
text: JSON.stringify({
|
|
1333
|
+
success: false,
|
|
1334
|
+
error: "Missing question ID.",
|
|
1335
|
+
hint: "Use ping_my_questions to see your questions and their IDs."
|
|
1336
|
+
}, null, 2)
|
|
1337
|
+
}]
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const data = await apiRequest(`/questions/${questionId}/responses`);
|
|
1341
|
+
if (data.responses.length === 0) {
|
|
1342
|
+
return {
|
|
1343
|
+
content: [{
|
|
1344
|
+
type: "text",
|
|
1345
|
+
text: JSON.stringify({
|
|
1346
|
+
success: true,
|
|
1347
|
+
question: data.question,
|
|
1348
|
+
responses: [],
|
|
1349
|
+
total: 0,
|
|
1350
|
+
message: "No responses yet.",
|
|
1351
|
+
hint: "Answers usually start arriving within a few hours. Check back later!"
|
|
1352
|
+
}, null, 2)
|
|
1353
|
+
}]
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
return {
|
|
1357
|
+
content: [{
|
|
1358
|
+
type: "text",
|
|
1359
|
+
text: JSON.stringify({
|
|
1360
|
+
success: true,
|
|
1361
|
+
question: data.question,
|
|
1362
|
+
responses: data.responses,
|
|
1363
|
+
total: data.total,
|
|
1364
|
+
message: `Found ${data.responses.length} response${data.responses.length !== 1 ? "s" : ""} to your question`
|
|
1365
|
+
}, null, 2)
|
|
1366
|
+
}]
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
// ────────────────────────────────────────────────────────
|
|
1370
|
+
// CLOSE QUESTION (Questioner)
|
|
1371
|
+
// ────────────────────────────────────────────────────────
|
|
1372
|
+
case "ping_close_question": {
|
|
1373
|
+
const auth = getAuth();
|
|
1374
|
+
if (!auth) {
|
|
1375
|
+
return {
|
|
1376
|
+
content: [{
|
|
1377
|
+
type: "text",
|
|
1378
|
+
text: JSON.stringify({
|
|
1379
|
+
success: false,
|
|
1380
|
+
error: "Not logged in.",
|
|
1381
|
+
hint: "Use ping_login to connect your GitHub account."
|
|
1382
|
+
}, null, 2)
|
|
1383
|
+
}]
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
const { questionId } = args || {};
|
|
1387
|
+
if (!questionId) {
|
|
1388
|
+
return {
|
|
1389
|
+
content: [{
|
|
1390
|
+
type: "text",
|
|
1391
|
+
text: JSON.stringify({
|
|
1392
|
+
success: false,
|
|
1393
|
+
error: "Missing question ID.",
|
|
1394
|
+
hint: "Use ping_my_questions to see your questions and their IDs."
|
|
1395
|
+
}, null, 2)
|
|
1396
|
+
}]
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
const data = await apiRequest(`/questions/${questionId}/close`, {
|
|
1400
|
+
method: "POST"
|
|
1401
|
+
});
|
|
1402
|
+
if (!data.success) {
|
|
1403
|
+
let errorMsg = data.error || "Failed to close question";
|
|
1404
|
+
let hint = "Please try again.";
|
|
1405
|
+
if (errorMsg.toLowerCase().includes("not found")) {
|
|
1406
|
+
hint = "Check that the question ID is correct using ping_my_questions.";
|
|
1407
|
+
} else if (errorMsg.toLowerCase().includes("already closed")) {
|
|
1408
|
+
hint = "This question has already been closed.";
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
content: [{
|
|
1412
|
+
type: "text",
|
|
1413
|
+
text: JSON.stringify({
|
|
1414
|
+
success: false,
|
|
1415
|
+
error: errorMsg,
|
|
1416
|
+
hint
|
|
1417
|
+
}, null, 2)
|
|
1418
|
+
}]
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
content: [{
|
|
1423
|
+
type: "text",
|
|
1424
|
+
text: JSON.stringify({
|
|
1425
|
+
success: true,
|
|
1426
|
+
refunded: data.refunded,
|
|
1427
|
+
message: `Question closed. ${data.refunded} refunded to your balance.`
|
|
1428
|
+
}, null, 2)
|
|
1429
|
+
}]
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
// ────────────────────────────────────────────────────────
|
|
1433
|
+
// DEPOSIT (Questioner)
|
|
1434
|
+
// ────────────────────────────────────────────────────────
|
|
1435
|
+
case "ping_deposit": {
|
|
1436
|
+
const auth = getAuth();
|
|
1437
|
+
if (!auth) {
|
|
1438
|
+
return {
|
|
1439
|
+
content: [{
|
|
1440
|
+
type: "text",
|
|
1441
|
+
text: JSON.stringify({
|
|
1442
|
+
success: false,
|
|
1443
|
+
error: "Not logged in.",
|
|
1444
|
+
hint: "Use ping_login to connect your GitHub account before adding funds."
|
|
1445
|
+
}, null, 2)
|
|
1446
|
+
}]
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
const { amountCents } = args || {};
|
|
1450
|
+
if (!amountCents || amountCents < 100) {
|
|
1451
|
+
return {
|
|
1452
|
+
content: [{
|
|
1453
|
+
type: "text",
|
|
1454
|
+
text: JSON.stringify({
|
|
1455
|
+
success: false,
|
|
1456
|
+
error: "Deposit amount too small.",
|
|
1457
|
+
hint: "Minimum deposit is $1.00 (100 cents). Example: 1000 = $10.00"
|
|
1458
|
+
}, null, 2)
|
|
1459
|
+
}]
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
const data = await apiRequest("/deposits", {
|
|
1463
|
+
method: "POST",
|
|
1464
|
+
body: JSON.stringify({ amountCents })
|
|
1465
|
+
});
|
|
1466
|
+
if (!data.success) {
|
|
1467
|
+
return {
|
|
1468
|
+
content: [{
|
|
1469
|
+
type: "text",
|
|
1470
|
+
text: JSON.stringify({
|
|
1471
|
+
success: false,
|
|
1472
|
+
error: data.error || "Deposit failed.",
|
|
1473
|
+
hint: "Please try again. If the problem persists, contact support."
|
|
1474
|
+
}, null, 2)
|
|
1475
|
+
}]
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{
|
|
1480
|
+
type: "text",
|
|
1481
|
+
text: JSON.stringify({
|
|
1482
|
+
success: true,
|
|
1483
|
+
deposited: data.deposited,
|
|
1484
|
+
newBalance: data.newBalance,
|
|
1485
|
+
message: `Deposited ${data.deposited}! Your new balance is ${data.newBalance}.`,
|
|
1486
|
+
nextStep: "You can now create questions with ping_create_question"
|
|
1487
|
+
}, null, 2)
|
|
1488
|
+
}]
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
// ────────────────────────────────────────────────────────
|
|
1492
|
+
// UNKNOWN TOOL
|
|
1493
|
+
// ────────────────────────────────────────────────────────
|
|
1494
|
+
default:
|
|
1495
|
+
return {
|
|
1496
|
+
content: [{
|
|
1497
|
+
type: "text",
|
|
1498
|
+
text: `Unknown tool: ${name}. Available tools: ping_login, ping_logout, ping_whoami, ping_set_wallet, ping_check_earnings, ping_list_questions, ping_submit_answer, ping_answer_flow, ping_claim_reward, ping_create_question, ping_my_questions, ping_view_responses, ping_close_question, ping_deposit`
|
|
1499
|
+
}],
|
|
1500
|
+
isError: true
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
const errorResponse = formatErrorResponse(error);
|
|
1505
|
+
return {
|
|
1506
|
+
content: [{
|
|
1507
|
+
type: "text",
|
|
1508
|
+
text: JSON.stringify(errorResponse, null, 2)
|
|
1509
|
+
}],
|
|
1510
|
+
isError: true
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
async function main() {
|
|
1515
|
+
const transport = new StdioServerTransport();
|
|
1516
|
+
await server.connect(transport);
|
|
1517
|
+
console.error("\u{1F3D3} Ping MCP server running");
|
|
1518
|
+
console.error(` API URL: ${API_BASE_URL}`);
|
|
1519
|
+
const auth = getAuth();
|
|
1520
|
+
if (auth) {
|
|
1521
|
+
console.error(` Logged in as: @${auth.handle}`);
|
|
1522
|
+
} else {
|
|
1523
|
+
const wallet = getWalletAddress();
|
|
1524
|
+
if (wallet) {
|
|
1525
|
+
console.error(` Wallet (legacy): ${wallet}`);
|
|
1526
|
+
} else {
|
|
1527
|
+
console.error(" Status: Not logged in (use ping_login)");
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
main().catch((error) => {
|
|
1532
|
+
console.error("Failed to start Ping MCP server:", error);
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
});
|