ping-mcp-server 0.1.5 → 0.1.6

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.js CHANGED
@@ -423,8 +423,8 @@ async function apiRequest(path2, options = {}) {
423
423
  if (fetchError.message.includes("fetch failed") || fetchError.message.includes("ECONNREFUSED") || fetchError.message.includes("ENOTFOUND") || fetchError.message.includes("ETIMEDOUT") || fetchError.cause) {
424
424
  throw new PingError(
425
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."
426
+ `Cannot connect to Ping API at ${API_BASE_URL}. Check your internet connection.`,
427
+ `Make sure you can reach ${API_BASE_URL} and try again.`
428
428
  );
429
429
  }
430
430
  }
@@ -459,7 +459,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
459
459
  // ────────────────────────────────────────────────────────
460
460
  {
461
461
  name: "ping_login",
462
- description: 'Login to Ping with GitHub. Opens a browser window for GitHub OAuth authentication. TRIGGERS: "ping login", "login to ping", "connect to ping", "sign in to ping", "start using ping". 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.',
462
+ description: 'Login to Ping with GitHub. Opens a browser window for GitHub OAuth authentication. TRIGGERS: "ping login", "login to ping", "connect to ping", "sign in to ping", "start using ping". 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. API: https://api.ping-money.com',
463
463
  inputSchema: {
464
464
  type: "object",
465
465
  properties: {},
@@ -477,7 +477,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
477
477
  },
478
478
  {
479
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.",
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. API: https://api.ping-money.com",
481
481
  inputSchema: {
482
482
  type: "object",
483
483
  properties: {},
@@ -513,20 +513,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
513
513
  required: []
514
514
  }
515
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
516
  {
531
517
  name: "ping_submit_answer",
532
518
  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.",
@@ -551,18 +537,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
551
537
  },
552
538
  {
553
539
  name: "ping_answer_flow",
554
- description: 'Start an interactive Q&A session to earn money answering Ping questions. TRIGGERS: "ping", "/ping", "answer questions", "earn money", "make money with ping", "start earning". 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).',
540
+ description: 'Start an interactive Q&A session to earn money answering Ping questions. TRIGGERS: "ping", "/ping", "answer questions", "earn money", "make money with ping", "start earning". Returns questions with suggested answers. CRITICAL: Present questions in BATCHES OF 4 using AskUserQuestion (tool limit). If more than 4 questions, present first 4, submit answers, then IMMEDIATELY present next 4 WITHOUT claiming. Continue batching until all questions are answered. Submit answers IN PARALLEL for each batch. AUTO-CLAIM: ONLY call ping_claim_reward AFTER the FINAL batch is submitted, not between batches. Requires GitHub login first (ping_login).',
555
541
  inputSchema: {
556
542
  type: "object",
557
543
  properties: {
558
544
  limit: {
559
545
  type: "number",
560
- description: "Number of questions to include in the session (default: 5)"
546
+ description: "Number of questions to include in the session (default: 50)"
561
547
  }
562
548
  },
563
549
  required: []
564
550
  }
565
551
  },
552
+ {
553
+ name: "ping_validate_batch",
554
+ description: "Validate if a batch of questions is still available to answer before showing to user. Checks if questions are active, not exhausted (maxResponses reached), and not already answered. Returns available questions with full data + list of exhausted question IDs. Use this before presenting questions to ensure they can actually be answered.",
555
+ inputSchema: {
556
+ type: "object",
557
+ properties: {
558
+ questionIds: {
559
+ type: "array",
560
+ items: { type: "string" },
561
+ description: 'Array of question IDs to validate (e.g., ["q1", "q2", "q3", "q4"])'
562
+ }
563
+ },
564
+ required: ["questionIds"]
565
+ }
566
+ },
566
567
  {
567
568
  name: "ping_claim_reward",
568
569
  description: 'Claim pending Ping earnings and send them to your crypto wallet on Base. TRIGGERS: "claim my ping", "withdraw from ping", "cash out", "get my money", "claim rewards". 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).',
@@ -897,43 +898,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
897
898
  };
898
899
  }
899
900
  // ────────────────────────────────────────────────────────
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
901
  // SUBMIT ANSWER
938
902
  // ────────────────────────────────────────────────────────
939
903
  case "ping_submit_answer": {
@@ -945,7 +909,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
945
909
  text: JSON.stringify({
946
910
  success: false,
947
911
  error: "Missing question ID.",
948
- hint: "Use ping_list_questions to see available questions with their IDs."
912
+ hint: "Use ping_answer_flow to see available questions and answer them."
949
913
  }, null, 2)
950
914
  }]
951
915
  };
@@ -1003,6 +967,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1003
967
  }]
1004
968
  };
1005
969
  }
970
+ if (data.error === "question_exhausted") {
971
+ return {
972
+ content: [{
973
+ type: "text",
974
+ text: JSON.stringify({
975
+ success: false,
976
+ error: "question_exhausted",
977
+ message: data.message || "This question is no longer accepting answers (maxResponses reached).",
978
+ hint: "Skip this question and continue with others in your batch."
979
+ })
980
+ }]
981
+ };
982
+ }
1006
983
  return {
1007
984
  content: [{
1008
985
  type: "text",
@@ -1051,7 +1028,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1051
1028
  }]
1052
1029
  };
1053
1030
  }
1054
- const { limit = 5 } = args || {};
1031
+ const { limit = 50 } = args || {};
1055
1032
  const data = await apiRequest(`/questions?limit=${limit}`);
1056
1033
  if (data.questions.length === 0) {
1057
1034
  return {
@@ -1093,23 +1070,86 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1093
1070
  // Full question data (present AFTER user confirms)
1094
1071
  questions: questionsWithSuggestions,
1095
1072
  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."
1073
+ overview: "This is a continuous batch validation workflow. Fetch 50 questions upfront, then validate+present in batches of 4 until all questions are exhausted.",
1074
+ step1_summary: 'FIRST show ONLY the oneLiner from summary with ~estimates, then ask "Ready to start?" Do NOT reveal questions yet.',
1075
+ step2_startBatching: "AFTER user confirms, START THE BATCH LOOP below:",
1076
+ batchLoop: {
1077
+ step1_selectNext: "Take the next 4 question IDs from the questions array (questions you haven't shown yet).",
1078
+ step2_validate: "Call ping_validate_batch with those 4 question IDs to check availability.",
1079
+ step3_handleExhausted: "If some questions are exhausted, use only the available ones returned (might be 0-4 questions).",
1080
+ step4_skip: "If 0 questions available in this batch, skip to next 4 question IDs and validate again.",
1081
+ step5_present: "Present available questions using AskUserQuestion (1-4 questions). Format: question text with reward in header, options are ONLY suggested answers.",
1082
+ step6_collect: "User answers the questions.",
1083
+ step7_submit: "Submit answers IN PARALLEL using multiple ping_submit_answer calls. preApproved=true for suggested answers, preApproved=false for custom.",
1084
+ step8_handleErrors: 'If some submissions fail with "question_exhausted" error, log them but continue.',
1085
+ step9_repeat: "If more questions remain in the original questions array, repeat from step1_selectNext. Otherwise, proceed to finalClaim."
1086
+ },
1087
+ finalClaim: "AFTER ALL batches complete (no more questions left to validate), call ping_claim_reward ONCE automatically.",
1088
+ claimRetry: "If ping_claim_reward fails, wait 2 seconds and retry ONCE. If it fails again, tell user earnings are saved.",
1089
+ completionMessage: "Show: total questions answered, total earned, any exhausted questions encountered, transaction hash, and basescan link."
1107
1090
  }
1108
1091
  }, null, 2)
1109
1092
  }]
1110
1093
  };
1111
1094
  }
1112
1095
  // ────────────────────────────────────────────────────────
1096
+ // VALIDATE BATCH
1097
+ // ────────────────────────────────────────────────────────
1098
+ case "ping_validate_batch": {
1099
+ const { questionIds } = args || {};
1100
+ if (!questionIds || !Array.isArray(questionIds) || questionIds.length === 0) {
1101
+ return {
1102
+ content: [{
1103
+ type: "text",
1104
+ text: JSON.stringify({
1105
+ success: false,
1106
+ error: "Invalid question IDs provided. Expected an array of question IDs."
1107
+ }, null, 2)
1108
+ }]
1109
+ };
1110
+ }
1111
+ const auth = getAuth();
1112
+ if (!auth) {
1113
+ return {
1114
+ content: [{
1115
+ type: "text",
1116
+ text: JSON.stringify({
1117
+ success: false,
1118
+ error: "Not logged in.",
1119
+ hint: "Use ping_login to connect your GitHub account first."
1120
+ }, null, 2)
1121
+ }]
1122
+ };
1123
+ }
1124
+ const data = await apiRequest("/questions/validate", {
1125
+ method: "POST",
1126
+ body: JSON.stringify({ questionIds })
1127
+ });
1128
+ const availableWithSuggestions = data.available.map((q) => {
1129
+ const suggestions = generateSuggestedAnswers(q.text, q.category);
1130
+ return {
1131
+ id: q.id,
1132
+ question: q.text,
1133
+ reward: q.reward,
1134
+ rewardCents: q.rewardCents,
1135
+ category: q.category,
1136
+ suggestedAnswers: suggestions
1137
+ };
1138
+ });
1139
+ return {
1140
+ content: [{
1141
+ type: "text",
1142
+ text: JSON.stringify({
1143
+ success: true,
1144
+ available: availableWithSuggestions,
1145
+ exhausted: data.exhausted,
1146
+ totalRemaining: data.totalRemaining,
1147
+ message: data.available.length === 0 ? "All questions in this batch are no longer available." : data.exhausted.length > 0 ? `${data.available.length} questions available, ${data.exhausted.length} exhausted.` : `All ${data.available.length} questions are available!`
1148
+ }, null, 2)
1149
+ }]
1150
+ };
1151
+ }
1152
+ // ────────────────────────────────────────────────────────
1113
1153
  // CLAIM REWARD
1114
1154
  // ────────────────────────────────────────────────────────
1115
1155
  case "ping_claim_reward": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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
@@ -688,8 +688,8 @@ async function apiRequest<T>(
688
688
  fetchError.cause) {
689
689
  throw new PingError(
690
690
  PingErrorCode.NETWORK_ERROR,
691
- 'Cannot connect to Ping API. Check your internet connection.',
692
- 'Make sure you have an active internet connection and try again.'
691
+ `Cannot connect to Ping API at ${API_BASE_URL}. Check your internet connection.`,
692
+ `Make sure you can reach ${API_BASE_URL} and try again.`
693
693
  );
694
694
  }
695
695
  }
@@ -743,7 +743,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
743
743
  'Login to Ping with GitHub. Opens a browser window for GitHub OAuth authentication. ' +
744
744
  'TRIGGERS: "ping login", "login to ping", "connect to ping", "sign in to ping", "start using ping". ' +
745
745
  'Use when the user wants to login, sign in, connect their account, or start using Ping. ' +
746
- 'After login, a wallet will be automatically created for the user.',
746
+ 'After login, a wallet will be automatically created for the user. ' +
747
+ 'API: https://api.ping-money.com',
747
748
  inputSchema: {
748
749
  type: 'object',
749
750
  properties: {},
@@ -765,7 +766,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
765
766
  name: 'ping_whoami',
766
767
  description:
767
768
  'Show your Ping account info - GitHub handle, wallet address, and balance. ' +
768
- 'Use when the user asks who they are logged in as, their account status, or profile info.',
769
+ 'Use when the user asks who they are logged in as, their account status, or profile info. ' +
770
+ 'API: https://api.ping-money.com',
769
771
  inputSchema: {
770
772
  type: 'object',
771
773
  properties: {},
@@ -808,23 +810,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
808
810
  required: [],
809
811
  },
810
812
  },
811
- {
812
- name: 'ping_list_questions',
813
- description:
814
- 'Show questions available to answer and earn money on Ping. ' +
815
- 'Use when the user wants to see earning opportunities, available questions, or ways to make money. ' +
816
- 'Returns questions with their rewards.',
817
- inputSchema: {
818
- type: 'object',
819
- properties: {
820
- limit: {
821
- type: 'number',
822
- description: 'Number of questions to show (default: 5)',
823
- },
824
- },
825
- required: [],
826
- },
827
- },
828
813
  {
829
814
  name: 'ping_submit_answer',
830
815
  description:
@@ -857,22 +842,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
857
842
  'Start an interactive Q&A session to earn money answering Ping questions. ' +
858
843
  'TRIGGERS: "ping", "/ping", "answer questions", "earn money", "make money with ping", "start earning". ' +
859
844
  'Returns questions with suggested answers. ' +
860
- 'CRITICAL: Present ALL questions in a SINGLE AskUserQuestion call (batched) to reduce latency. ' +
861
- 'Use the questions array parameter with multiple question objects. ' +
862
- 'After user answers, submit ALL answers IN PARALLEL for speed. ' +
863
- 'AUTO-CLAIM: After all submissions complete, AUTOMATICALLY call ping_claim_reward to send earnings to wallet. ' +
845
+ 'CRITICAL: Present questions in BATCHES OF 4 using AskUserQuestion (tool limit). ' +
846
+ 'If more than 4 questions, present first 4, submit answers, then IMMEDIATELY present next 4 WITHOUT claiming. ' +
847
+ 'Continue batching until all questions are answered. ' +
848
+ 'Submit answers IN PARALLEL for each batch. ' +
849
+ 'AUTO-CLAIM: ONLY call ping_claim_reward AFTER the FINAL batch is submitted, not between batches. ' +
864
850
  'Requires GitHub login first (ping_login).',
865
851
  inputSchema: {
866
852
  type: 'object',
867
853
  properties: {
868
854
  limit: {
869
855
  type: 'number',
870
- description: 'Number of questions to include in the session (default: 5)',
856
+ description: 'Number of questions to include in the session (default: 50)',
871
857
  },
872
858
  },
873
859
  required: [],
874
860
  },
875
861
  },
862
+ {
863
+ name: 'ping_validate_batch',
864
+ description:
865
+ 'Validate if a batch of questions is still available to answer before showing to user. ' +
866
+ 'Checks if questions are active, not exhausted (maxResponses reached), and not already answered. ' +
867
+ 'Returns available questions with full data + list of exhausted question IDs. ' +
868
+ 'Use this before presenting questions to ensure they can actually be answered.',
869
+ inputSchema: {
870
+ type: 'object',
871
+ properties: {
872
+ questionIds: {
873
+ type: 'array',
874
+ items: { type: 'string' },
875
+ description: 'Array of question IDs to validate (e.g., ["q1", "q2", "q3", "q4"])',
876
+ },
877
+ },
878
+ required: ['questionIds'],
879
+ },
880
+ },
876
881
  {
877
882
  name: 'ping_claim_reward',
878
883
  description:
@@ -1289,57 +1294,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1289
1294
  };
1290
1295
  }
1291
1296
 
1292
- // ────────────────────────────────────────────────────────
1293
- // LIST QUESTIONS
1294
- // ────────────────────────────────────────────────────────
1295
- case 'ping_list_questions': {
1296
- const { limit = 5 } = (args || {}) as { limit?: number };
1297
-
1298
- const data = await apiRequest<{
1299
- questions: Array<{
1300
- id: string;
1301
- text: string;
1302
- reward: string;
1303
- rewardCents: number;
1304
- category: string;
1305
- asker: string;
1306
- }>;
1307
- total: number;
1308
- }>(`/questions?limit=${limit}`);
1309
-
1310
- if (data.questions.length === 0) {
1311
- return {
1312
- content: [{
1313
- type: 'text',
1314
- text: JSON.stringify({
1315
- success: true,
1316
- questions: [],
1317
- message: 'No questions available right now.',
1318
- hint: 'New questions are added regularly. Check back soon, or use ping_create_question to ask your own!',
1319
- }, null, 2),
1320
- }],
1321
- };
1322
- }
1323
-
1324
- return {
1325
- content: [{
1326
- type: 'text',
1327
- text: JSON.stringify({
1328
- success: true,
1329
- questions: data.questions.map(q => ({
1330
- id: q.id,
1331
- question: q.text,
1332
- reward: q.reward,
1333
- category: q.category,
1334
- asker: q.asker,
1335
- })),
1336
- totalAvailable: data.total,
1337
- message: `Found ${data.questions.length} question${data.questions.length !== 1 ? 's' : ''} you can answer!`,
1338
- }, null, 2),
1339
- }],
1340
- };
1341
- }
1342
-
1343
1297
  // ────────────────────────────────────────────────────────
1344
1298
  // SUBMIT ANSWER
1345
1299
  // ────────────────────────────────────────────────────────
@@ -1357,7 +1311,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1357
1311
  text: JSON.stringify({
1358
1312
  success: false,
1359
1313
  error: 'Missing question ID.',
1360
- hint: 'Use ping_list_questions to see available questions with their IDs.',
1314
+ hint: 'Use ping_answer_flow to see available questions and answer them.',
1361
1315
  }, null, 2),
1362
1316
  }],
1363
1317
  };
@@ -1438,6 +1392,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1438
1392
  };
1439
1393
  }
1440
1394
 
1395
+ // Check if question is exhausted (reached maxResponses)
1396
+ if (data.error === 'question_exhausted') {
1397
+ return {
1398
+ content: [{
1399
+ type: 'text',
1400
+ text: JSON.stringify({
1401
+ success: false,
1402
+ error: 'question_exhausted',
1403
+ message: data.message || 'This question is no longer accepting answers (maxResponses reached).',
1404
+ hint: 'Skip this question and continue with others in your batch.',
1405
+ }),
1406
+ }],
1407
+ };
1408
+ }
1409
+
1441
1410
  return {
1442
1411
  content: [{
1443
1412
  type: 'text',
@@ -1495,7 +1464,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1495
1464
  };
1496
1465
  }
1497
1466
 
1498
- const { limit = 5 } = (args || {}) as { limit?: number };
1467
+ const { limit = 50 } = (args || {}) as { limit?: number };
1499
1468
 
1500
1469
  // Fetch questions from API
1501
1470
  const data = await apiRequest<{
@@ -1557,23 +1526,108 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1557
1526
  // Full question data (present AFTER user confirms)
1558
1527
  questions: questionsWithSuggestions,
1559
1528
  instructions: {
1560
- step1_summary: 'FIRST show ONLY the oneLiner from summary, then ask "Ready to start?" Do NOT reveal questions yet.',
1561
- step2_questions: 'AFTER user confirms, BATCH ALL QUESTIONS in a SINGLE AskUserQuestion call (max 4 per call due to tool limit).',
1562
- 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.',
1563
- suggestedAnswerSubmission: 'When user selects a suggested answer, call ping_submit_answer with preApproved=true for instant approval.',
1564
- customAnswerSubmission: 'When user types custom text via "Other", call ping_submit_answer with preApproved=false (AI review).',
1565
- parallelSubmission: 'After collecting all answers, submit them IN PARALLEL using multiple ping_submit_answer calls in one message for speed.',
1566
- skipBehavior: 'If user selects "Other" and leaves blank or says skip, do not submit an answer for that question.',
1567
- errorHandling: 'If some answer submissions fail, still proceed with auto-claim for the successful ones. Report which answers failed and why.',
1568
- 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.',
1569
- 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.',
1570
- completionMessage: 'After claim completes, show: total earned, transaction hash, and basescan link.',
1529
+ overview: 'This is a continuous batch validation workflow. Fetch 50 questions upfront, then validate+present in batches of 4 until all questions are exhausted.',
1530
+ step1_summary: 'FIRST show ONLY the oneLiner from summary with ~estimates, then ask "Ready to start?" Do NOT reveal questions yet.',
1531
+ step2_startBatching: 'AFTER user confirms, START THE BATCH LOOP below:',
1532
+ batchLoop: {
1533
+ step1_selectNext: 'Take the next 4 question IDs from the questions array (questions you haven\'t shown yet).',
1534
+ step2_validate: 'Call ping_validate_batch with those 4 question IDs to check availability.',
1535
+ step3_handleExhausted: 'If some questions are exhausted, use only the available ones returned (might be 0-4 questions).',
1536
+ step4_skip: 'If 0 questions available in this batch, skip to next 4 question IDs and validate again.',
1537
+ step5_present: 'Present available questions using AskUserQuestion (1-4 questions). Format: question text with reward in header, options are ONLY suggested answers.',
1538
+ step6_collect: 'User answers the questions.',
1539
+ step7_submit: 'Submit answers IN PARALLEL using multiple ping_submit_answer calls. preApproved=true for suggested answers, preApproved=false for custom.',
1540
+ step8_handleErrors: 'If some submissions fail with "question_exhausted" error, log them but continue.',
1541
+ step9_repeat: 'If more questions remain in the original questions array, repeat from step1_selectNext. Otherwise, proceed to finalClaim.',
1542
+ },
1543
+ finalClaim: 'AFTER ALL batches complete (no more questions left to validate), call ping_claim_reward ONCE automatically.',
1544
+ claimRetry: 'If ping_claim_reward fails, wait 2 seconds and retry ONCE. If it fails again, tell user earnings are saved.',
1545
+ completionMessage: 'Show: total questions answered, total earned, any exhausted questions encountered, transaction hash, and basescan link.',
1571
1546
  },
1572
1547
  }, null, 2),
1573
1548
  }],
1574
1549
  };
1575
1550
  }
1576
1551
 
1552
+ // ────────────────────────────────────────────────────────
1553
+ // VALIDATE BATCH
1554
+ // ────────────────────────────────────────────────────────
1555
+ case 'ping_validate_batch': {
1556
+ const { questionIds } = (args || {}) as { questionIds: string[] };
1557
+
1558
+ if (!questionIds || !Array.isArray(questionIds) || questionIds.length === 0) {
1559
+ return {
1560
+ content: [{
1561
+ type: 'text',
1562
+ text: JSON.stringify({
1563
+ success: false,
1564
+ error: 'Invalid question IDs provided. Expected an array of question IDs.',
1565
+ }, null, 2),
1566
+ }],
1567
+ };
1568
+ }
1569
+
1570
+ const auth = getAuth();
1571
+ if (!auth) {
1572
+ return {
1573
+ content: [{
1574
+ type: 'text',
1575
+ text: JSON.stringify({
1576
+ success: false,
1577
+ error: 'Not logged in.',
1578
+ hint: 'Use ping_login to connect your GitHub account first.',
1579
+ }, null, 2),
1580
+ }],
1581
+ };
1582
+ }
1583
+
1584
+ // Call the validate endpoint
1585
+ const data = await apiRequest<{
1586
+ available: Array<{
1587
+ id: string;
1588
+ text: string;
1589
+ reward: string;
1590
+ rewardCents: number;
1591
+ category: string;
1592
+ }>;
1593
+ exhausted: string[];
1594
+ totalRemaining: number;
1595
+ }>('/questions/validate', {
1596
+ method: 'POST',
1597
+ body: JSON.stringify({ questionIds }),
1598
+ });
1599
+
1600
+ // Generate suggested answers for available questions
1601
+ const availableWithSuggestions = data.available.map(q => {
1602
+ const suggestions = generateSuggestedAnswers(q.text, q.category);
1603
+ return {
1604
+ id: q.id,
1605
+ question: q.text,
1606
+ reward: q.reward,
1607
+ rewardCents: q.rewardCents,
1608
+ category: q.category,
1609
+ suggestedAnswers: suggestions,
1610
+ };
1611
+ });
1612
+
1613
+ return {
1614
+ content: [{
1615
+ type: 'text',
1616
+ text: JSON.stringify({
1617
+ success: true,
1618
+ available: availableWithSuggestions,
1619
+ exhausted: data.exhausted,
1620
+ totalRemaining: data.totalRemaining,
1621
+ message: data.available.length === 0
1622
+ ? 'All questions in this batch are no longer available.'
1623
+ : data.exhausted.length > 0
1624
+ ? `${data.available.length} questions available, ${data.exhausted.length} exhausted.`
1625
+ : `All ${data.available.length} questions are available!`,
1626
+ }, null, 2),
1627
+ }],
1628
+ };
1629
+ }
1630
+
1577
1631
  // ────────────────────────────────────────────────────────
1578
1632
  // CLAIM REWARD
1579
1633
  // ────────────────────────────────────────────────────────
@@ -1,29 +0,0 @@
1
- {
2
- "name": "ping-mcp-server",
3
- "version": "0.1.4",
4
- "description": "MCP server that gives Claude Code the ability to interact with Ping",
5
- "type": "module",
6
- "bin": {
7
- "ping-mcp-server": "./dist/init.js",
8
- "ping-mcp": "./dist/index.js",
9
- "ping-init": "./dist/init.js"
10
- },
11
- "scripts": {
12
- "dev": "tsx watch src/index.ts",
13
- "build": "tsup src/index.ts src/init.ts --format esm --dts",
14
- "start": "node dist/index.js",
15
- "test": "vitest",
16
- "lint": "eslint src/",
17
- "clean": "rm -rf dist"
18
- },
19
- "dependencies": {
20
- "@modelcontextprotocol/sdk": "^1.0.0",
21
- "zod": "^3.22.0"
22
- },
23
- "devDependencies": {
24
- "@types/node": "^25.0.3",
25
- "tsup": "^8.0.0",
26
- "tsx": "^4.0.0",
27
- "vitest": "^1.0.0"
28
- }
29
- }