ping-mcp-server 0.1.19 → 0.1.22

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.
Files changed (3) hide show
  1. package/dist/index.js +256 -94
  2. package/package.json +1 -1
  3. package/src/index.ts +278 -82
package/dist/index.js CHANGED
@@ -75,6 +75,66 @@ function clearAuth() {
75
75
  delete config.serverWalletAddress;
76
76
  saveConfig(config);
77
77
  }
78
+ var CONTEXT_FILE = path.join(CONFIG_DIR, "context.json");
79
+ function loadContext() {
80
+ try {
81
+ if (fs.existsSync(CONTEXT_FILE)) {
82
+ const data = fs.readFileSync(CONTEXT_FILE, "utf-8");
83
+ return JSON.parse(data);
84
+ }
85
+ } catch (error) {
86
+ }
87
+ return { problems: [], techStack: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
88
+ }
89
+ function saveContext(context) {
90
+ try {
91
+ if (!fs.existsSync(CONFIG_DIR)) {
92
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
93
+ }
94
+ context.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
95
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(context, null, 2));
96
+ } catch (error) {
97
+ console.error("Failed to save context:", error);
98
+ }
99
+ }
100
+ function generateQuestionSuggestions(context) {
101
+ const suggestions = [];
102
+ const templates = {
103
+ decision: [
104
+ "Choosing between {X} - what would you pick and why?",
105
+ "Is {X} worth the added complexity?",
106
+ "Anyone regret going with {X}? What would you do differently?"
107
+ ],
108
+ debugging: [
109
+ "Seeing weird behavior with {X} - anyone dealt with this?",
110
+ "What's the gotcha everyone hits with {X}?"
111
+ ],
112
+ architecture: [
113
+ "What's the production-ready way to handle {X}?",
114
+ "Is {X} overkill for a small team?",
115
+ "How do you think about {X} vs just keeping it simple?"
116
+ ],
117
+ tooling: [
118
+ "What's the vibe on {X} in 2025? Worth adopting?",
119
+ "Anyone actually using {X} in production? How's it going?",
120
+ "What do people actually use for {X}? (not what's popular on Twitter)"
121
+ ],
122
+ other: [
123
+ "What's your take on {X}?",
124
+ "How do you think about {X}?"
125
+ ]
126
+ };
127
+ for (const problem of context.problems.slice(0, 3)) {
128
+ const categoryTemplates = templates[problem.category] || templates.other;
129
+ const template = categoryTemplates[Math.floor(Math.random() * categoryTemplates.length)];
130
+ suggestions.push(template.replace("{X}", problem.summary));
131
+ }
132
+ if (context.techStack.length > 0) {
133
+ const tech = context.techStack[0];
134
+ suggestions.push(`What's something you wish you knew before using ${tech}?`);
135
+ }
136
+ return suggestions;
137
+ }
78
138
  function generateSuggestedAnswers(questionText, category) {
79
139
  const lowerQuestion = questionText.toLowerCase();
80
140
  if (lowerQuestion.includes("coffee") && lowerQuestion.includes("tea")) {
@@ -503,6 +563,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
503
563
  }
504
564
  },
505
565
  // ────────────────────────────────────────────────────────
566
+ // CONTEXT TOOLS (for smart question suggestions)
567
+ // ────────────────────────────────────────────────────────
568
+ {
569
+ name: "ping_log_context",
570
+ description: "Log what the user is working on for smarter question suggestions. Call this when you notice the user is: making a decision between options, debugging something tricky, discussing architecture, or evaluating tools. This helps Ping suggest relevant questions later.",
571
+ inputSchema: {
572
+ type: "object",
573
+ properties: {
574
+ summary: {
575
+ type: "string",
576
+ description: 'Brief summary of the problem/decision (e.g., "choosing between Prisma and Drizzle")'
577
+ },
578
+ category: {
579
+ type: "string",
580
+ enum: ["decision", "debugging", "architecture", "tooling", "other"],
581
+ description: "Type of problem"
582
+ },
583
+ tags: {
584
+ type: "array",
585
+ items: { type: "string" },
586
+ description: 'Related technologies or concepts (e.g., ["orm", "database", "typescript"])'
587
+ }
588
+ },
589
+ required: ["summary", "category"]
590
+ }
591
+ },
592
+ {
593
+ name: "ping_suggest_questions",
594
+ description: "Get question suggestions based on what the user has been working on. Call this when: (1) user wants to create a question but doesn't know what to ask, (2) you notice something that would benefit from human expertise/taste rather than AI knowledge, (3) proactively when the user has been wrestling with a decision. Returns taste/judgment questions that humans answer better than AI.",
595
+ inputSchema: {
596
+ type: "object",
597
+ properties: {},
598
+ required: []
599
+ }
600
+ },
601
+ // ────────────────────────────────────────────────────────
506
602
  // LEGACY: SET WALLET (for users without GitHub auth)
507
603
  // ────────────────────────────────────────────────────────
508
604
  {
@@ -820,7 +916,7 @@ Use ping_login to sign in with a different account.`
820
916
  // ────────────────────────────────────────────────────────
821
917
  case "ping_welcome": {
822
918
  const auth = getAuth();
823
- const MCP_VERSION = "0.1.19";
919
+ const MCP_VERSION = "0.1.21";
824
920
  let userLine = "\u274C Not logged in";
825
921
  let arenaBadge = "";
826
922
  if (auth) {
@@ -836,9 +932,15 @@ Use ping_login to sign in with a different account.`
836
932
  let questionsAvailable = 0;
837
933
  let answersToday = 0;
838
934
  let claimedToday = "$0.00";
935
+ if (auth) {
936
+ try {
937
+ const questionsData = await apiRequest("/questions?limit=1");
938
+ questionsAvailable = questionsData.totalAvailable;
939
+ } catch {
940
+ }
941
+ }
839
942
  try {
840
943
  const stats = await apiRequest("/stats");
841
- questionsAvailable = stats.questions.active;
842
944
  answersToday = stats.responses.today;
843
945
  claimedToday = stats.rewards.claimedToday;
844
946
  } catch {
@@ -898,6 +1000,89 @@ ${settingsUrl}`
898
1000
  };
899
1001
  }
900
1002
  // ────────────────────────────────────────────────────────
1003
+ // LOG CONTEXT (for smart question suggestions)
1004
+ // ────────────────────────────────────────────────────────
1005
+ case "ping_log_context": {
1006
+ const { summary, category, tags = [] } = args || {};
1007
+ if (!summary || !category) {
1008
+ return {
1009
+ content: [{
1010
+ type: "text",
1011
+ text: "\u274C Missing required fields: summary and category"
1012
+ }]
1013
+ };
1014
+ }
1015
+ const context = loadContext();
1016
+ context.problems.unshift({
1017
+ summary,
1018
+ category,
1019
+ tags,
1020
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1021
+ });
1022
+ context.problems = context.problems.slice(0, 10);
1023
+ for (const tag of tags) {
1024
+ if (!context.techStack.includes(tag)) {
1025
+ context.techStack.unshift(tag);
1026
+ }
1027
+ }
1028
+ context.techStack = context.techStack.slice(0, 10);
1029
+ saveContext(context);
1030
+ return {
1031
+ content: [{
1032
+ type: "text",
1033
+ text: `\u{1F4DD} Logged: "${summary}" (${category})
1034
+
1035
+ This will help suggest relevant questions later.`
1036
+ }]
1037
+ };
1038
+ }
1039
+ // ────────────────────────────────────────────────────────
1040
+ // SUGGEST QUESTIONS (based on stored context)
1041
+ // ────────────────────────────────────────────────────────
1042
+ case "ping_suggest_questions": {
1043
+ const context = loadContext();
1044
+ if (context.problems.length === 0 && context.techStack.length === 0) {
1045
+ return {
1046
+ content: [{
1047
+ type: "text",
1048
+ text: `\u{1F4A1} No context stored yet!
1049
+
1050
+ As you work, I'll learn what you're dealing with and suggest relevant questions.
1051
+
1052
+ **Generic taste/judgment questions:**
1053
+ \u2022 "What's something you wish you knew earlier in your career?"
1054
+ \u2022 "What tool do you use that most people don't know about?"
1055
+ \u2022 "What's overrated in tech right now?"`
1056
+ }]
1057
+ };
1058
+ }
1059
+ const suggestions = generateQuestionSuggestions(context);
1060
+ let output = `\u{1F4A1} **Question suggestions based on your recent work:**
1061
+
1062
+ `;
1063
+ suggestions.forEach((q, i) => {
1064
+ output += `${i + 1}. "${q}"
1065
+ `;
1066
+ });
1067
+ output += `
1068
+ **Your recent context:**
1069
+ `;
1070
+ output += `\u2022 Problems: ${context.problems.slice(0, 3).map((p) => p.summary).join(", ")}
1071
+ `;
1072
+ if (context.techStack.length > 0) {
1073
+ output += `\u2022 Tech: ${context.techStack.slice(0, 5).join(", ")}
1074
+ `;
1075
+ }
1076
+ output += `
1077
+ _Pick one to ask, edit it, or write your own!_`;
1078
+ return {
1079
+ content: [{
1080
+ type: "text",
1081
+ text: output
1082
+ }]
1083
+ };
1084
+ }
1085
+ // ────────────────────────────────────────────────────────
901
1086
  // SET WALLET (Legacy)
902
1087
  // ────────────────────────────────────────────────────────
903
1088
  case "ping_set_wallet": {
@@ -1334,11 +1519,9 @@ ID: ${data.question?.id}`;
1334
1519
  return {
1335
1520
  content: [{
1336
1521
  type: "text",
1337
- text: JSON.stringify({
1338
- success: false,
1339
- error: "Not logged in.",
1340
- hint: "Use ping_login to connect your GitHub account."
1341
- }, null, 2)
1522
+ text: `\u274C Not logged in
1523
+
1524
+ Use ping_login to connect your GitHub account.`
1342
1525
  }]
1343
1526
  };
1344
1527
  }
@@ -1348,24 +1531,27 @@ ID: ${data.question?.id}`;
1348
1531
  return {
1349
1532
  content: [{
1350
1533
  type: "text",
1351
- text: JSON.stringify({
1352
- success: true,
1353
- questions: [],
1354
- message: "You haven't created any questions yet.",
1355
- hint: "Use ping_create_question to ask your first question and get answers from the community!"
1356
- }, null, 2)
1534
+ text: `\u{1F4ED} No questions yet
1535
+
1536
+ Use ping_create_question to ask your first question!`
1357
1537
  }]
1358
1538
  };
1359
1539
  }
1540
+ let output = `\u{1F4CB} Your Questions (${data.questions.length} total)
1541
+
1542
+ `;
1543
+ data.questions.forEach((q, i) => {
1544
+ const statusEmoji = q.status === "active" ? "\u{1F7E2}" : "\u26AA";
1545
+ output += `${i + 1}. ${statusEmoji} "${q.text}"
1546
+ `;
1547
+ output += ` Reward: ${q.reward} \xB7 Responses: ${q.responseCount}/${q.maxResponses} \xB7 ID: ${q.id}
1548
+
1549
+ `;
1550
+ });
1360
1551
  return {
1361
1552
  content: [{
1362
1553
  type: "text",
1363
- text: JSON.stringify({
1364
- success: true,
1365
- questions: data.questions,
1366
- total: data.total,
1367
- message: `Found ${data.questions.length} question${data.questions.length !== 1 ? "s" : ""}`
1368
- }, null, 2)
1554
+ text: output.trim()
1369
1555
  }]
1370
1556
  };
1371
1557
  }
@@ -1378,11 +1564,9 @@ ID: ${data.question?.id}`;
1378
1564
  return {
1379
1565
  content: [{
1380
1566
  type: "text",
1381
- text: JSON.stringify({
1382
- success: false,
1383
- error: "Not logged in.",
1384
- hint: "Use ping_login to connect your GitHub account."
1385
- }, null, 2)
1567
+ text: `\u274C Not logged in
1568
+
1569
+ Use ping_login to connect your GitHub account.`
1386
1570
  }]
1387
1571
  };
1388
1572
  }
@@ -1391,11 +1575,9 @@ ID: ${data.question?.id}`;
1391
1575
  return {
1392
1576
  content: [{
1393
1577
  type: "text",
1394
- text: JSON.stringify({
1395
- success: false,
1396
- error: "Missing question ID.",
1397
- hint: "Use ping_my_questions to see your questions and their IDs."
1398
- }, null, 2)
1578
+ text: `\u274C Missing question ID
1579
+
1580
+ Use ping_my_questions to see your questions and their IDs.`
1399
1581
  }]
1400
1582
  };
1401
1583
  }
@@ -1404,27 +1586,27 @@ ID: ${data.question?.id}`;
1404
1586
  return {
1405
1587
  content: [{
1406
1588
  type: "text",
1407
- text: JSON.stringify({
1408
- success: true,
1409
- question: data.question,
1410
- responses: [],
1411
- total: 0,
1412
- message: "No responses yet.",
1413
- hint: "Answers usually start arriving within a few hours. Check back later!"
1414
- }, null, 2)
1589
+ text: `\u{1F4ED} No responses yet for "${data.question.text}"
1590
+
1591
+ Answers usually arrive within a few hours. Check back later!`
1415
1592
  }]
1416
1593
  };
1417
1594
  }
1595
+ let output = `\u{1F4AC} Responses to "${data.question.text}" (${data.responses.length} total)
1596
+
1597
+ `;
1598
+ data.responses.forEach((r, i) => {
1599
+ const statusEmoji = r.status === "approved" ? "\u2705" : r.status === "rejected" ? "\u274C" : "\u23F3";
1600
+ output += `${i + 1}. ${statusEmoji} "${r.answerText.slice(0, 100)}${r.answerText.length > 100 ? "..." : ""}"
1601
+ `;
1602
+ if (r.aiScore) output += ` Score: ${r.aiScore}/100
1603
+ `;
1604
+ output += "\n";
1605
+ });
1418
1606
  return {
1419
1607
  content: [{
1420
1608
  type: "text",
1421
- text: JSON.stringify({
1422
- success: true,
1423
- question: data.question,
1424
- responses: data.responses,
1425
- total: data.total,
1426
- message: `Found ${data.responses.length} response${data.responses.length !== 1 ? "s" : ""} to your question`
1427
- }, null, 2)
1609
+ text: output.trim()
1428
1610
  }]
1429
1611
  };
1430
1612
  }
@@ -1437,11 +1619,9 @@ ID: ${data.question?.id}`;
1437
1619
  return {
1438
1620
  content: [{
1439
1621
  type: "text",
1440
- text: JSON.stringify({
1441
- success: false,
1442
- error: "Not logged in.",
1443
- hint: "Use ping_login to connect your GitHub account."
1444
- }, null, 2)
1622
+ text: `\u274C Not logged in
1623
+
1624
+ Use ping_login to connect your GitHub account.`
1445
1625
  }]
1446
1626
  };
1447
1627
  }
@@ -1450,11 +1630,9 @@ ID: ${data.question?.id}`;
1450
1630
  return {
1451
1631
  content: [{
1452
1632
  type: "text",
1453
- text: JSON.stringify({
1454
- success: false,
1455
- error: "Missing question ID.",
1456
- hint: "Use ping_my_questions to see your questions and their IDs."
1457
- }, null, 2)
1633
+ text: `\u274C Missing question ID
1634
+
1635
+ Use ping_my_questions to see your questions and their IDs.`
1458
1636
  }]
1459
1637
  };
1460
1638
  }
@@ -1462,32 +1640,20 @@ ID: ${data.question?.id}`;
1462
1640
  method: "POST"
1463
1641
  });
1464
1642
  if (!data.success) {
1465
- let errorMsg = data.error || "Failed to close question";
1466
- let hint = "Please try again.";
1467
- if (errorMsg.toLowerCase().includes("not found")) {
1468
- hint = "Check that the question ID is correct using ping_my_questions.";
1469
- } else if (errorMsg.toLowerCase().includes("already closed")) {
1470
- hint = "This question has already been closed.";
1471
- }
1643
+ const errorMsg = data.error || "Failed to close question";
1472
1644
  return {
1473
1645
  content: [{
1474
1646
  type: "text",
1475
- text: JSON.stringify({
1476
- success: false,
1477
- error: errorMsg,
1478
- hint
1479
- }, null, 2)
1647
+ text: `\u274C ${errorMsg}`
1480
1648
  }]
1481
1649
  };
1482
1650
  }
1483
1651
  return {
1484
1652
  content: [{
1485
1653
  type: "text",
1486
- text: JSON.stringify({
1487
- success: true,
1488
- refunded: data.refunded,
1489
- message: `Question closed. ${data.refunded} refunded to your balance.`
1490
- }, null, 2)
1654
+ text: `\u2705 Question closed
1655
+
1656
+ ${data.refunded} refunded to your balance.`
1491
1657
  }]
1492
1658
  };
1493
1659
  }
@@ -1500,11 +1666,9 @@ ID: ${data.question?.id}`;
1500
1666
  return {
1501
1667
  content: [{
1502
1668
  type: "text",
1503
- text: JSON.stringify({
1504
- success: false,
1505
- error: "Not logged in.",
1506
- hint: "Use ping_login to connect your GitHub account before adding funds."
1507
- }, null, 2)
1669
+ text: `\u274C Not logged in
1670
+
1671
+ Use ping_login to connect your GitHub account first.`
1508
1672
  }]
1509
1673
  };
1510
1674
  }
@@ -1513,11 +1677,9 @@ ID: ${data.question?.id}`;
1513
1677
  return {
1514
1678
  content: [{
1515
1679
  type: "text",
1516
- text: JSON.stringify({
1517
- success: false,
1518
- error: "Deposit amount too small.",
1519
- hint: "Minimum deposit is $1.00 (100 cents). Example: 1000 = $10.00"
1520
- }, null, 2)
1680
+ text: `\u274C Deposit amount too small
1681
+
1682
+ Minimum deposit is $1.00 (100 cents). Example: 1000 = $10.00`
1521
1683
  }]
1522
1684
  };
1523
1685
  }
@@ -1529,24 +1691,18 @@ ID: ${data.question?.id}`;
1529
1691
  return {
1530
1692
  content: [{
1531
1693
  type: "text",
1532
- text: JSON.stringify({
1533
- success: false,
1534
- error: data.error || "Deposit failed.",
1535
- hint: "Please try again. If the problem persists, contact support."
1536
- }, null, 2)
1694
+ text: `\u274C ${data.error || "Deposit failed"}`
1537
1695
  }]
1538
1696
  };
1539
1697
  }
1540
1698
  return {
1541
1699
  content: [{
1542
1700
  type: "text",
1543
- text: JSON.stringify({
1544
- success: true,
1545
- deposited: data.deposited,
1546
- newBalance: data.newBalance,
1547
- message: `Deposited ${data.deposited}! Your new balance is ${data.newBalance}.`,
1548
- nextStep: "You can now create questions with ping_create_question"
1549
- }, null, 2)
1701
+ text: `\u2705 Deposited ${data.deposited}!
1702
+
1703
+ New balance: ${data.newBalance}
1704
+
1705
+ You can now create questions with ping_create_question`
1550
1706
  }]
1551
1707
  };
1552
1708
  }
@@ -1564,10 +1720,16 @@ ID: ${data.question?.id}`;
1564
1720
  }
1565
1721
  } catch (error) {
1566
1722
  const errorResponse = formatErrorResponse(error);
1723
+ let errorText = `\u274C ${errorResponse.error}`;
1724
+ if (errorResponse.hint) {
1725
+ errorText += `
1726
+
1727
+ \u{1F4A1} ${errorResponse.hint}`;
1728
+ }
1567
1729
  return {
1568
1730
  content: [{
1569
1731
  type: "text",
1570
- text: JSON.stringify(errorResponse, null, 2)
1732
+ text: errorText
1571
1733
  }],
1572
1734
  isError: true
1573
1735
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-mcp-server",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
4
4
  "description": "MCP server that gives Claude Code the ability to interact with Ping",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -134,6 +134,104 @@ function clearAuth(): void {
134
134
  saveConfig(config);
135
135
  }
136
136
 
137
+ // ============================================================
138
+ // CONTEXT STORAGE (for smart question suggestions)
139
+ // ============================================================
140
+
141
+ /**
142
+ * Context file stores what the user has been working on across sessions.
143
+ * This enables Ping to suggest relevant questions based on actual problems.
144
+ */
145
+ const CONTEXT_FILE = path.join(CONFIG_DIR, 'context.json');
146
+
147
+ interface PingContext {
148
+ // Problems/decisions the user is working through
149
+ problems: Array<{
150
+ summary: string;
151
+ category: 'decision' | 'debugging' | 'architecture' | 'tooling' | 'other';
152
+ tags: string[];
153
+ timestamp: string;
154
+ }>;
155
+ // Inferred tech stack from their projects
156
+ techStack: string[];
157
+ // Last updated timestamp
158
+ lastUpdated: string;
159
+ }
160
+
161
+ function loadContext(): PingContext {
162
+ try {
163
+ if (fs.existsSync(CONTEXT_FILE)) {
164
+ const data = fs.readFileSync(CONTEXT_FILE, 'utf-8');
165
+ return JSON.parse(data);
166
+ }
167
+ } catch (error) {
168
+ // Ignore errors, return empty context
169
+ }
170
+ return { problems: [], techStack: [], lastUpdated: new Date().toISOString() };
171
+ }
172
+
173
+ function saveContext(context: PingContext): void {
174
+ try {
175
+ if (!fs.existsSync(CONFIG_DIR)) {
176
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
177
+ }
178
+ context.lastUpdated = new Date().toISOString();
179
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(context, null, 2));
180
+ } catch (error) {
181
+ console.error('Failed to save context:', error);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Generate question suggestions based on stored context.
187
+ * Focuses on taste/judgment questions that humans answer better than AI.
188
+ */
189
+ function generateQuestionSuggestions(context: PingContext): string[] {
190
+ const suggestions: string[] = [];
191
+
192
+ // Question templates focused on taste/judgment (not technical how-to)
193
+ const templates = {
194
+ decision: [
195
+ 'Choosing between {X} - what would you pick and why?',
196
+ 'Is {X} worth the added complexity?',
197
+ 'Anyone regret going with {X}? What would you do differently?',
198
+ ],
199
+ debugging: [
200
+ 'Seeing weird behavior with {X} - anyone dealt with this?',
201
+ 'What\'s the gotcha everyone hits with {X}?',
202
+ ],
203
+ architecture: [
204
+ 'What\'s the production-ready way to handle {X}?',
205
+ 'Is {X} overkill for a small team?',
206
+ 'How do you think about {X} vs just keeping it simple?',
207
+ ],
208
+ tooling: [
209
+ 'What\'s the vibe on {X} in 2025? Worth adopting?',
210
+ 'Anyone actually using {X} in production? How\'s it going?',
211
+ 'What do people actually use for {X}? (not what\'s popular on Twitter)',
212
+ ],
213
+ other: [
214
+ 'What\'s your take on {X}?',
215
+ 'How do you think about {X}?',
216
+ ],
217
+ };
218
+
219
+ // Generate suggestions from recent problems
220
+ for (const problem of context.problems.slice(0, 3)) {
221
+ const categoryTemplates = templates[problem.category] || templates.other;
222
+ const template = categoryTemplates[Math.floor(Math.random() * categoryTemplates.length)];
223
+ suggestions.push(template.replace('{X}', problem.summary));
224
+ }
225
+
226
+ // Add tech stack based suggestions if we have them
227
+ if (context.techStack.length > 0) {
228
+ const tech = context.techStack[0];
229
+ suggestions.push(`What's something you wish you knew before using ${tech}?`);
230
+ }
231
+
232
+ return suggestions;
233
+ }
234
+
137
235
  // ============================================================
138
236
  // SUGGESTED ANSWER GENERATION
139
237
  // ============================================================
@@ -798,6 +896,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
798
896
  },
799
897
  },
800
898
  // ────────────────────────────────────────────────────────
899
+ // CONTEXT TOOLS (for smart question suggestions)
900
+ // ────────────────────────────────────────────────────────
901
+ {
902
+ name: 'ping_log_context',
903
+ description:
904
+ 'Log what the user is working on for smarter question suggestions. ' +
905
+ 'Call this when you notice the user is: making a decision between options, ' +
906
+ 'debugging something tricky, discussing architecture, or evaluating tools. ' +
907
+ 'This helps Ping suggest relevant questions later.',
908
+ inputSchema: {
909
+ type: 'object',
910
+ properties: {
911
+ summary: {
912
+ type: 'string',
913
+ description: 'Brief summary of the problem/decision (e.g., "choosing between Prisma and Drizzle")',
914
+ },
915
+ category: {
916
+ type: 'string',
917
+ enum: ['decision', 'debugging', 'architecture', 'tooling', 'other'],
918
+ description: 'Type of problem',
919
+ },
920
+ tags: {
921
+ type: 'array',
922
+ items: { type: 'string' },
923
+ description: 'Related technologies or concepts (e.g., ["orm", "database", "typescript"])',
924
+ },
925
+ },
926
+ required: ['summary', 'category'],
927
+ },
928
+ },
929
+ {
930
+ name: 'ping_suggest_questions',
931
+ description:
932
+ 'Get question suggestions based on what the user has been working on. ' +
933
+ 'Call this when: (1) user wants to create a question but doesn\'t know what to ask, ' +
934
+ '(2) you notice something that would benefit from human expertise/taste rather than AI knowledge, ' +
935
+ '(3) proactively when the user has been wrestling with a decision. ' +
936
+ 'Returns taste/judgment questions that humans answer better than AI.',
937
+ inputSchema: {
938
+ type: 'object',
939
+ properties: {},
940
+ required: [],
941
+ },
942
+ },
943
+ // ────────────────────────────────────────────────────────
801
944
  // LEGACY: SET WALLET (for users without GitHub auth)
802
945
  // ────────────────────────────────────────────────────────
803
946
  {
@@ -1188,7 +1331,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1188
1331
  // ────────────────────────────────────────────────────────
1189
1332
  case 'ping_welcome': {
1190
1333
  const auth = getAuth();
1191
- const MCP_VERSION = '0.1.19';
1334
+ const MCP_VERSION = '0.1.21';
1192
1335
 
1193
1336
  // Get user handle and connections
1194
1337
  let userLine = '❌ Not logged in';
@@ -1210,18 +1353,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1210
1353
  }
1211
1354
  }
1212
1355
 
1213
- // Get stats (with fallback defaults)
1356
+ // Get USER-SPECIFIC available questions (not platform-wide)
1214
1357
  let questionsAvailable = 0;
1215
1358
  let answersToday = 0;
1216
1359
  let claimedToday = '$0.00';
1217
1360
 
1361
+ // First get questions available FOR THIS USER
1362
+ if (auth) {
1363
+ try {
1364
+ const questionsData = await apiRequest<{
1365
+ questions: Array<{ id: string }>;
1366
+ totalAvailable: number;
1367
+ }>('/questions?limit=1');
1368
+ questionsAvailable = questionsData.totalAvailable;
1369
+ } catch {
1370
+ // Fall back to 0
1371
+ }
1372
+ }
1373
+
1374
+ // Then get platform stats for answers/claimed (these are user-specific)
1218
1375
  try {
1219
1376
  const stats = await apiRequest<{
1220
- questions: { active: number };
1221
1377
  responses: { today: number };
1222
1378
  rewards: { claimedToday: string };
1223
1379
  }>('/stats');
1224
- questionsAvailable = stats.questions.active;
1225
1380
  answersToday = stats.responses.today;
1226
1381
  claimedToday = stats.rewards.claimedToday;
1227
1382
  } catch {
@@ -1296,6 +1451,97 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1296
1451
  };
1297
1452
  }
1298
1453
 
1454
+ // ────────────────────────────────────────────────────────
1455
+ // LOG CONTEXT (for smart question suggestions)
1456
+ // ────────────────────────────────────────────────────────
1457
+ case 'ping_log_context': {
1458
+ const { summary, category, tags = [] } = (args || {}) as {
1459
+ summary: string;
1460
+ category: 'decision' | 'debugging' | 'architecture' | 'tooling' | 'other';
1461
+ tags?: string[];
1462
+ };
1463
+
1464
+ if (!summary || !category) {
1465
+ return {
1466
+ content: [{
1467
+ type: 'text',
1468
+ text: '❌ Missing required fields: summary and category',
1469
+ }],
1470
+ };
1471
+ }
1472
+
1473
+ // Load existing context
1474
+ const context = loadContext();
1475
+
1476
+ // Add new problem (keep last 10)
1477
+ context.problems.unshift({
1478
+ summary,
1479
+ category,
1480
+ tags,
1481
+ timestamp: new Date().toISOString(),
1482
+ });
1483
+ context.problems = context.problems.slice(0, 10);
1484
+
1485
+ // Update tech stack from tags
1486
+ for (const tag of tags) {
1487
+ if (!context.techStack.includes(tag)) {
1488
+ context.techStack.unshift(tag);
1489
+ }
1490
+ }
1491
+ context.techStack = context.techStack.slice(0, 10);
1492
+
1493
+ // Save
1494
+ saveContext(context);
1495
+
1496
+ return {
1497
+ content: [{
1498
+ type: 'text',
1499
+ text: `📝 Logged: "${summary}" (${category})\n\nThis will help suggest relevant questions later.`,
1500
+ }],
1501
+ };
1502
+ }
1503
+
1504
+ // ────────────────────────────────────────────────────────
1505
+ // SUGGEST QUESTIONS (based on stored context)
1506
+ // ────────────────────────────────────────────────────────
1507
+ case 'ping_suggest_questions': {
1508
+ const context = loadContext();
1509
+
1510
+ // If no context yet, return generic suggestions
1511
+ if (context.problems.length === 0 && context.techStack.length === 0) {
1512
+ return {
1513
+ content: [{
1514
+ type: 'text',
1515
+ text: `💡 No context stored yet!\n\nAs you work, I'll learn what you're dealing with and suggest relevant questions.\n\n**Generic taste/judgment questions:**\n• "What's something you wish you knew earlier in your career?"\n• "What tool do you use that most people don't know about?"\n• "What's overrated in tech right now?"`,
1516
+ }],
1517
+ };
1518
+ }
1519
+
1520
+ // Generate suggestions
1521
+ const suggestions = generateQuestionSuggestions(context);
1522
+
1523
+ // Format output
1524
+ let output = `💡 **Question suggestions based on your recent work:**\n\n`;
1525
+ suggestions.forEach((q, i) => {
1526
+ output += `${i + 1}. "${q}"\n`;
1527
+ });
1528
+
1529
+ output += `\n**Your recent context:**\n`;
1530
+ output += `• Problems: ${context.problems.slice(0, 3).map(p => p.summary).join(', ')}\n`;
1531
+ if (context.techStack.length > 0) {
1532
+ output += `• Tech: ${context.techStack.slice(0, 5).join(', ')}\n`;
1533
+ }
1534
+
1535
+ output += `\n_Pick one to ask, edit it, or write your own!_`;
1536
+
1537
+ return {
1538
+ content: [{
1539
+ type: 'text',
1540
+ text: output,
1541
+ }],
1542
+ };
1543
+ }
1544
+
1299
1545
  // ────────────────────────────────────────────────────────
1300
1546
  // SET WALLET (Legacy)
1301
1547
  // ────────────────────────────────────────────────────────
@@ -1924,11 +2170,7 @@ Wallet: ${data.walletAddress}`,
1924
2170
  return {
1925
2171
  content: [{
1926
2172
  type: 'text',
1927
- text: JSON.stringify({
1928
- success: false,
1929
- error: 'Not logged in.',
1930
- hint: 'Use ping_login to connect your GitHub account.',
1931
- }, null, 2),
2173
+ text: `❌ Not logged in\n\nUse ping_login to connect your GitHub account.`,
1932
2174
  }],
1933
2175
  };
1934
2176
  }
@@ -1939,11 +2181,7 @@ Wallet: ${data.walletAddress}`,
1939
2181
  return {
1940
2182
  content: [{
1941
2183
  type: 'text',
1942
- text: JSON.stringify({
1943
- success: false,
1944
- error: 'Missing question ID.',
1945
- hint: 'Use ping_my_questions to see your questions and their IDs.',
1946
- }, null, 2),
2184
+ text: `❌ Missing question ID\n\nUse ping_my_questions to see your questions and their IDs.`,
1947
2185
  }],
1948
2186
  };
1949
2187
  }
@@ -1969,28 +2207,24 @@ Wallet: ${data.walletAddress}`,
1969
2207
  return {
1970
2208
  content: [{
1971
2209
  type: 'text',
1972
- text: JSON.stringify({
1973
- success: true,
1974
- question: data.question,
1975
- responses: [],
1976
- total: 0,
1977
- message: 'No responses yet.',
1978
- hint: 'Answers usually start arriving within a few hours. Check back later!',
1979
- }, null, 2),
2210
+ text: `📭 No responses yet for "${data.question.text}"\n\nAnswers usually arrive within a few hours. Check back later!`,
1980
2211
  }],
1981
2212
  };
1982
2213
  }
1983
2214
 
2215
+ // Build human-readable response list
2216
+ let output = `💬 Responses to "${data.question.text}" (${data.responses.length} total)\n\n`;
2217
+ data.responses.forEach((r, i) => {
2218
+ const statusEmoji = r.status === 'approved' ? '✅' : r.status === 'rejected' ? '❌' : '⏳';
2219
+ output += `${i + 1}. ${statusEmoji} "${r.answerText.slice(0, 100)}${r.answerText.length > 100 ? '...' : ''}"\n`;
2220
+ if (r.aiScore) output += ` Score: ${r.aiScore}/100\n`;
2221
+ output += '\n';
2222
+ });
2223
+
1984
2224
  return {
1985
2225
  content: [{
1986
2226
  type: 'text',
1987
- text: JSON.stringify({
1988
- success: true,
1989
- question: data.question,
1990
- responses: data.responses,
1991
- total: data.total,
1992
- message: `Found ${data.responses.length} response${data.responses.length !== 1 ? 's' : ''} to your question`,
1993
- }, null, 2),
2227
+ text: output.trim(),
1994
2228
  }],
1995
2229
  };
1996
2230
  }
@@ -2005,11 +2239,7 @@ Wallet: ${data.walletAddress}`,
2005
2239
  return {
2006
2240
  content: [{
2007
2241
  type: 'text',
2008
- text: JSON.stringify({
2009
- success: false,
2010
- error: 'Not logged in.',
2011
- hint: 'Use ping_login to connect your GitHub account.',
2012
- }, null, 2),
2242
+ text: `❌ Not logged in\n\nUse ping_login to connect your GitHub account.`,
2013
2243
  }],
2014
2244
  };
2015
2245
  }
@@ -2020,11 +2250,7 @@ Wallet: ${data.walletAddress}`,
2020
2250
  return {
2021
2251
  content: [{
2022
2252
  type: 'text',
2023
- text: JSON.stringify({
2024
- success: false,
2025
- error: 'Missing question ID.',
2026
- hint: 'Use ping_my_questions to see your questions and their IDs.',
2027
- }, null, 2),
2253
+ text: `❌ Missing question ID\n\nUse ping_my_questions to see your questions and their IDs.`,
2028
2254
  }],
2029
2255
  };
2030
2256
  }
@@ -2038,23 +2264,11 @@ Wallet: ${data.walletAddress}`,
2038
2264
  });
2039
2265
 
2040
2266
  if (!data.success) {
2041
- let errorMsg = data.error || 'Failed to close question';
2042
- let hint = 'Please try again.';
2043
-
2044
- if (errorMsg.toLowerCase().includes('not found')) {
2045
- hint = 'Check that the question ID is correct using ping_my_questions.';
2046
- } else if (errorMsg.toLowerCase().includes('already closed')) {
2047
- hint = 'This question has already been closed.';
2048
- }
2049
-
2267
+ const errorMsg = data.error || 'Failed to close question';
2050
2268
  return {
2051
2269
  content: [{
2052
2270
  type: 'text',
2053
- text: JSON.stringify({
2054
- success: false,
2055
- error: errorMsg,
2056
- hint,
2057
- }, null, 2),
2271
+ text: `❌ ${errorMsg}`,
2058
2272
  }],
2059
2273
  };
2060
2274
  }
@@ -2062,11 +2276,7 @@ Wallet: ${data.walletAddress}`,
2062
2276
  return {
2063
2277
  content: [{
2064
2278
  type: 'text',
2065
- text: JSON.stringify({
2066
- success: true,
2067
- refunded: data.refunded,
2068
- message: `Question closed. ${data.refunded} refunded to your balance.`,
2069
- }, null, 2),
2279
+ text: `✅ Question closed\n\n${data.refunded} refunded to your balance.`,
2070
2280
  }],
2071
2281
  };
2072
2282
  }
@@ -2081,11 +2291,7 @@ Wallet: ${data.walletAddress}`,
2081
2291
  return {
2082
2292
  content: [{
2083
2293
  type: 'text',
2084
- text: JSON.stringify({
2085
- success: false,
2086
- error: 'Not logged in.',
2087
- hint: 'Use ping_login to connect your GitHub account before adding funds.',
2088
- }, null, 2),
2294
+ text: `❌ Not logged in\n\nUse ping_login to connect your GitHub account first.`,
2089
2295
  }],
2090
2296
  };
2091
2297
  }
@@ -2096,11 +2302,7 @@ Wallet: ${data.walletAddress}`,
2096
2302
  return {
2097
2303
  content: [{
2098
2304
  type: 'text',
2099
- text: JSON.stringify({
2100
- success: false,
2101
- error: 'Deposit amount too small.',
2102
- hint: 'Minimum deposit is $1.00 (100 cents). Example: 1000 = $10.00',
2103
- }, null, 2),
2305
+ text: `❌ Deposit amount too small\n\nMinimum deposit is $1.00 (100 cents). Example: 1000 = $10.00`,
2104
2306
  }],
2105
2307
  };
2106
2308
  }
@@ -2119,11 +2321,7 @@ Wallet: ${data.walletAddress}`,
2119
2321
  return {
2120
2322
  content: [{
2121
2323
  type: 'text',
2122
- text: JSON.stringify({
2123
- success: false,
2124
- error: data.error || 'Deposit failed.',
2125
- hint: 'Please try again. If the problem persists, contact support.',
2126
- }, null, 2),
2324
+ text: `❌ ${data.error || 'Deposit failed'}`,
2127
2325
  }],
2128
2326
  };
2129
2327
  }
@@ -2131,13 +2329,7 @@ Wallet: ${data.walletAddress}`,
2131
2329
  return {
2132
2330
  content: [{
2133
2331
  type: 'text',
2134
- text: JSON.stringify({
2135
- success: true,
2136
- deposited: data.deposited,
2137
- newBalance: data.newBalance,
2138
- message: `Deposited ${data.deposited}! Your new balance is ${data.newBalance}.`,
2139
- nextStep: 'You can now create questions with ping_create_question',
2140
- }, null, 2),
2332
+ text: `✅ Deposited ${data.deposited}!\n\nNew balance: ${data.newBalance}\n\nYou can now create questions with ping_create_question`,
2141
2333
  }],
2142
2334
  };
2143
2335
  }
@@ -2155,12 +2347,16 @@ Wallet: ${data.walletAddress}`,
2155
2347
  };
2156
2348
  }
2157
2349
  } catch (error) {
2158
- // Use the formatErrorResponse helper for consistent error formatting
2350
+ // Human-readable error messages
2159
2351
  const errorResponse = formatErrorResponse(error);
2352
+ let errorText = `❌ ${errorResponse.error}`;
2353
+ if (errorResponse.hint) {
2354
+ errorText += `\n\n💡 ${errorResponse.hint}`;
2355
+ }
2160
2356
  return {
2161
2357
  content: [{
2162
2358
  type: 'text',
2163
- text: JSON.stringify(errorResponse, null, 2),
2359
+ text: errorText,
2164
2360
  }],
2165
2361
  isError: true,
2166
2362
  };