@solworks/poll-mcp 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.
package/dist/index.js CHANGED
@@ -3,9 +3,12 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
5
  import { PollClient } from '@solworks/poll-api-client';
6
+ import { createRequire } from 'node:module';
6
7
  import { TOOL_DEFINITIONS } from './tools/index.js';
7
8
  import { RESOURCE_DEFINITIONS } from './resources/index.js';
8
9
  import { PROMPT_DEFINITIONS } from './prompts/index.js';
10
+ const require = createRequire(import.meta.url);
11
+ const { version: PKG_VERSION } = require('../package.json');
9
12
  /**
10
13
  * Convert a JSON Schema property type to a Zod schema.
11
14
  */
@@ -23,7 +26,7 @@ function jsonPropToZod(prop) {
23
26
  }
24
27
  }
25
28
  export function createServer(config, clientOverride) {
26
- const server = new McpServer({ name: 'poll-fun', version: '0.1.18' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
29
+ const server = new McpServer({ name: 'poll-fun', version: PKG_VERSION }, { capabilities: { tools: {}, resources: { subscribe: true, listChanged: true }, prompts: {} } });
27
30
  const client = clientOverride ??
28
31
  new PollClient({
29
32
  apiUrl: config?.apiUrl ?? 'https://api.poll.fun',
@@ -405,7 +405,7 @@ export const TOOL_DEFINITIONS = {
405
405
  if (!options || !Array.isArray(options) || options.length < 2)
406
406
  return errorResult('Missing or invalid parameter: options (must be an array with at least 2 items)');
407
407
  try {
408
- const bet = await client.createBet({
408
+ const result = await client.createBet({
409
409
  question,
410
410
  expectedUserCount: 2,
411
411
  minimumVoteCount: 2,
@@ -414,7 +414,7 @@ export const TOOL_DEFINITIONS = {
414
414
  minimumWagerAmount: args.amount,
415
415
  allWagersDueBy: args.expiresAt,
416
416
  });
417
- return textResult(`Bet created successfully!\n Question: ${bet.question}\n Options: ${Array.isArray(bet.options) ? bet.options.join(', ') : 'For / Against'}\n Status: ${bet.status}\n Address: ${bet.betAddress}`);
417
+ return textResult(`Bet created successfully!\n Question: ${question}\n Options: ${options.join(', ')}\n Address: ${result.relatedAddress}`);
418
418
  }
419
419
  catch (err) {
420
420
  return errorResult(err.message);
@@ -437,6 +437,10 @@ export const TOOL_DEFINITIONS = {
437
437
  if (!betAddress)
438
438
  return errorResult('Missing required parameter: betAddress');
439
439
  try {
440
+ const bet = await client.getBet(betAddress);
441
+ if (!bet.isPublic) {
442
+ return errorResult('This is a private bet. You can only join private bets via the share link in the app.');
443
+ }
440
444
  await client.joinBet(betAddress);
441
445
  return textResult(`Successfully joined bet: ${betAddress}`);
442
446
  }
@@ -486,19 +490,23 @@ export const TOOL_DEFINITIONS = {
486
490
  type: 'object',
487
491
  properties: {
488
492
  betAddress: { type: 'string', description: 'Address of the bet' },
489
- wagerAddress: { type: 'string', description: 'Address of the wager to cancel' },
490
493
  },
491
- required: ['betAddress', 'wagerAddress'],
494
+ required: ['betAddress'],
492
495
  },
493
496
  requiredScope: 'bet:write',
494
497
  async handler(args, client) {
495
498
  const betAddress = args.betAddress;
496
- const wagerAddress = args.wagerAddress;
497
499
  if (!betAddress)
498
500
  return errorResult('Missing required parameter: betAddress');
499
- if (!wagerAddress)
500
- return errorResult('Missing required parameter: wagerAddress');
501
501
  try {
502
+ const wagers = await client.getMyWagers();
503
+ const activeWager = wagers.find((w) => {
504
+ const addr = w.betAddress ?? w.programBetBetAddress;
505
+ return addr === betAddress && w.amount > 0;
506
+ });
507
+ if (!activeWager) {
508
+ return errorResult(`You don't have an active wager on bet: ${betAddress}`);
509
+ }
502
510
  await client.cancelWager({ marketPubkey: betAddress });
503
511
  return textResult(`Wager cancelled successfully for bet: ${betAddress}`);
504
512
  }
@@ -523,6 +531,11 @@ export const TOOL_DEFINITIONS = {
523
531
  if (!betAddress)
524
532
  return errorResult('Missing required parameter: betAddress');
525
533
  try {
534
+ const bet = await client.getBet(betAddress);
535
+ const status = (bet.status ?? '').toLowerCase();
536
+ if (status === 'resolving' || status === 'resolved' || status === 'distributed') {
537
+ return errorResult(`Vote has already been initiated for this bet (status: ${bet.status})`);
538
+ }
526
539
  await client.initiateVote(betAddress);
527
540
  return textResult(`Vote initiated for bet: ${betAddress}`);
528
541
  }
@@ -551,6 +564,14 @@ export const TOOL_DEFINITIONS = {
551
564
  if (optionIndex === undefined || optionIndex === null)
552
565
  return errorResult('Missing required parameter: optionIndex');
553
566
  try {
567
+ const [bet, account] = await Promise.all([
568
+ client.getBet(betAddress),
569
+ client.getAccount(),
570
+ ]);
571
+ const alreadyVoted = Array.isArray(bet.votes) && bet.votes.some((v) => v.user === account.uuid || v.userUuid === account.uuid);
572
+ if (alreadyVoted) {
573
+ return errorResult('You have already voted on this bet');
574
+ }
554
575
  const outcome = optionIndex === 0 ? 'for' : 'against';
555
576
  await client.vote({ marketPubkey: betAddress, outcome: outcome });
556
577
  return textResult(`Vote cast successfully on bet ${betAddress} for option ${optionIndex} (${outcome})`);
@@ -576,6 +597,24 @@ export const TOOL_DEFINITIONS = {
576
597
  if (!betAddress)
577
598
  return errorResult('Missing required parameter: betAddress');
578
599
  try {
600
+ const bet = await client.getBet(betAddress);
601
+ const status = (bet.status ?? '').toLowerCase();
602
+ if (status === 'distributed') {
603
+ return errorResult('This bet has already been settled and distributed');
604
+ }
605
+ if (status !== 'resolving' && status !== 'resolved') {
606
+ return errorResult(`Bet cannot be settled in its current status: ${bet.status}. Vote must be initiated first.`);
607
+ }
608
+ const wagerUsers = new Set((bet.wagers ?? []).filter((w) => w.amount > 0).map((w) => w.user));
609
+ const votedUsers = new Set((bet.votes ?? []).map((v) => v.user));
610
+ const notVoted = [...wagerUsers].filter((u) => !votedUsers.has(u));
611
+ if (notVoted.length > 0) {
612
+ const names = notVoted.map((u) => {
613
+ const wager = bet.wagers.find((w) => w.user === u);
614
+ return wager?.userDisplayName ?? u;
615
+ });
616
+ return errorResult(`Cannot settle: ${notVoted.length} participant(s) have not voted yet: ${names.join(', ')}`);
617
+ }
579
618
  await client.settleBet(betAddress);
580
619
  return textResult(`Bet settled successfully: ${betAddress}`);
581
620
  }
@@ -610,6 +649,36 @@ export const TOOL_DEFINITIONS = {
610
649
  },
611
650
  },
612
651
  // ── Social write tools (scope: social:write) ──────────────────────
652
+ get_friend_requests: {
653
+ name: 'get_friend_requests',
654
+ description: 'Get your pending friend requests (received and sent)',
655
+ inputSchema: { type: 'object', properties: {} },
656
+ requiredScope: 'read',
657
+ async handler(_args, client) {
658
+ try {
659
+ const [received, sent] = await Promise.all([
660
+ client.getReceivedFriendRequests(),
661
+ client.getSentFriendRequests(),
662
+ ]);
663
+ const receivedLines = received.length > 0
664
+ ? received.map((r, i) => ` ${i + 1}. From: ${r.sender?.displayName ?? 'Unknown'} (${r.sender?.uuid ?? '?'}) — Request ID: ${r.id} — ${r.createdAt}`)
665
+ : [' None'];
666
+ const sentLines = sent.length > 0
667
+ ? sent.map((r, i) => ` ${i + 1}. To: ${r.receiver?.displayName ?? 'Unknown'} (${r.receiver?.uuid ?? '?'}) — Request ID: ${r.id} — ${r.createdAt}`)
668
+ : [' None'];
669
+ return textResult([
670
+ `Received Friend Requests (${received.length}):`,
671
+ ...receivedLines,
672
+ '',
673
+ `Sent Friend Requests (${sent.length}):`,
674
+ ...sentLines,
675
+ ].join('\n'));
676
+ }
677
+ catch (err) {
678
+ return errorResult(err.message);
679
+ }
680
+ },
681
+ },
613
682
  send_friend_request: {
614
683
  name: 'send_friend_request',
615
684
  description: 'Send a friend request to another user',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solworks/poll-mcp",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
4
4
  "description": "MCP server for Poll.fun. See documentation at https://dev.poll.fun",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,13 +19,14 @@ function mockClient(overrides: Partial<PollClient> = {}): PollClient {
19
19
  getXpBalance: vi.fn().mockResolvedValue({ xp: 1000, error: null }),
20
20
  getUsdcBalance: vi.fn().mockResolvedValue({ usdc: 50, error: null }),
21
21
  getTrendingBets: vi.fn().mockResolvedValue([
22
- { question: 'Trending bet 1', totalVolume: 100 },
22
+ { question: 'Trending bet 1', totalOiFor: 50, totalOiAgainst: 50, betAddress: 'addr1' },
23
23
  ]),
24
24
  getLeaderboard: vi.fn().mockResolvedValue([
25
25
  { userId: 1, uuid: 'u1', displayName: 'Top', rank: 1, points: 999 },
26
26
  ]),
27
27
  getMyBets: vi.fn().mockResolvedValue([]),
28
28
  getMyWagers: vi.fn().mockResolvedValue([]),
29
+ trackEvent: vi.fn().mockResolvedValue(undefined),
29
30
  ...overrides,
30
31
  } as unknown as PollClient;
31
32
  }
@@ -81,6 +82,7 @@ describe('MCP Server integration', () => {
81
82
  'update_display_name',
82
83
  'send_friend_request',
83
84
  'respond_friend_request',
85
+ 'get_friend_requests',
84
86
  'favourite_bet',
85
87
  'unfavourite_bet',
86
88
  ];
@@ -88,7 +90,7 @@ describe('MCP Server integration', () => {
88
90
  for (const name of expectedTools) {
89
91
  expect(toolNames).toContain(name);
90
92
  }
91
- expect(tools.length).toBe(27);
93
+ expect(tools.length).toBe(28);
92
94
  });
93
95
 
94
96
  it('does NOT expose excluded tools', async () => {
@@ -10,8 +10,8 @@ describe('Bet tool handlers', () => {
10
10
  it('get_trending_bets returns formatted list', async () => {
11
11
  const client = mockClient({
12
12
  getTrendingBets: vi.fn().mockResolvedValue([
13
- { question: 'Will BTC hit 100k?', totalVolume: 5000 },
14
- { question: 'ETH above 5k?', totalVolume: 3000 },
13
+ { question: 'Will BTC hit 100k?', totalOiFor: 3000, totalOiAgainst: 2000, betAddress: 'addr1' },
14
+ { question: 'ETH above 5k?', totalOiFor: 2000, totalOiAgainst: 1000, betAddress: 'addr2' },
15
15
  ]),
16
16
  });
17
17
  const result = await TOOL_DEFINITIONS.get_trending_bets.handler({}, client);
@@ -28,7 +28,8 @@ describe('Bet tool handlers', () => {
28
28
  question: 'Will it rain tomorrow?',
29
29
  status: 'OPEN',
30
30
  options: ['Yes', 'No'],
31
- totalVolume: 1000,
31
+ totalOiFor: 600,
32
+ totalOiAgainst: 400,
32
33
  createdAt: '2026-01-01T00:00:00Z',
33
34
  betAddress: 'addr123',
34
35
  }),
@@ -80,7 +81,8 @@ describe('Bet tool handlers', () => {
80
81
 
81
82
  it('place_wager with valid params', async () => {
82
83
  const placeWagerFn = vi.fn().mockResolvedValue({});
83
- const client = mockClient({ placeWager: placeWagerFn });
84
+ const validateWagerSideFn = vi.fn().mockResolvedValue(undefined);
85
+ const client = mockClient({ placeWager: placeWagerFn, validateWagerSide: validateWagerSideFn });
84
86
  const result = await TOOL_DEFINITIONS.place_wager.handler(
85
87
  { betAddress: 'addr1', optionIndex: 0, amount: 50 },
86
88
  client
@@ -88,9 +90,9 @@ describe('Bet tool handlers', () => {
88
90
  expect(result.isError).toBeUndefined();
89
91
  expect(result.content[0].text).toContain('Wager placed successfully');
90
92
  expect(placeWagerFn).toHaveBeenCalledWith({
91
- betAddress: 'addr1',
92
- optionIndex: 0,
93
+ marketPubkey: 'addr1',
93
94
  amount: 50,
95
+ side: 'for',
94
96
  });
95
97
  });
96
98