@solworks/poll-mcp 0.1.20 → 0.1.26

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: { listChanged: true }, prompts: {} } });
27
30
  const client = clientOverride ??
28
31
  new PollClient({
29
32
  apiUrl: config?.apiUrl ?? 'https://api.poll.fun',
@@ -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,18 @@ 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 status = (bet.status ?? '').toLowerCase();
572
+ if (status !== 'resolving') {
573
+ return errorResult(`Cannot vote: voting has not been initiated for this bet (status: ${bet.status}). Use initiate_vote first.`);
574
+ }
575
+ const alreadyVoted = Array.isArray(bet.votes) && bet.votes.some((v) => v.user === account.uuid || v.userUuid === account.uuid);
576
+ if (alreadyVoted) {
577
+ return errorResult('You have already voted on this bet');
578
+ }
554
579
  const outcome = optionIndex === 0 ? 'for' : 'against';
555
580
  await client.vote({ marketPubkey: betAddress, outcome: outcome });
556
581
  return textResult(`Vote cast successfully on bet ${betAddress} for option ${optionIndex} (${outcome})`);
@@ -576,6 +601,24 @@ export const TOOL_DEFINITIONS = {
576
601
  if (!betAddress)
577
602
  return errorResult('Missing required parameter: betAddress');
578
603
  try {
604
+ const bet = await client.getBet(betAddress);
605
+ const status = (bet.status ?? '').toLowerCase();
606
+ if (status === 'distributed') {
607
+ return errorResult('This bet has already been settled and distributed');
608
+ }
609
+ if (status !== 'resolving' && status !== 'resolved') {
610
+ return errorResult(`Bet cannot be settled in its current status: ${bet.status}. Vote must be initiated first.`);
611
+ }
612
+ const wagerUsers = new Set((bet.wagers ?? []).filter((w) => w.amount > 0).map((w) => w.user));
613
+ const votedUsers = new Set((bet.votes ?? []).map((v) => v.user));
614
+ const notVoted = [...wagerUsers].filter((u) => !votedUsers.has(u));
615
+ if (notVoted.length > 0) {
616
+ const names = notVoted.map((u) => {
617
+ const wager = bet.wagers.find((w) => w.user === u);
618
+ return wager?.userDisplayName ?? u;
619
+ });
620
+ return errorResult(`Cannot settle: ${notVoted.length} participant(s) have not voted yet: ${names.join(', ')}`);
621
+ }
579
622
  await client.settleBet(betAddress);
580
623
  return textResult(`Bet settled successfully: ${betAddress}`);
581
624
  }
@@ -610,6 +653,36 @@ export const TOOL_DEFINITIONS = {
610
653
  },
611
654
  },
612
655
  // ── Social write tools (scope: social:write) ──────────────────────
656
+ get_friend_requests: {
657
+ name: 'get_friend_requests',
658
+ description: 'Get your pending friend requests (received and sent)',
659
+ inputSchema: { type: 'object', properties: {} },
660
+ requiredScope: 'read',
661
+ async handler(_args, client) {
662
+ try {
663
+ const [received, sent] = await Promise.all([
664
+ client.getReceivedFriendRequests(),
665
+ client.getSentFriendRequests(),
666
+ ]);
667
+ const receivedLines = received.length > 0
668
+ ? received.map((r, i) => ` ${i + 1}. From: ${r.sender?.displayName ?? 'Unknown'} (${r.sender?.uuid ?? '?'}) — Request ID: ${r.id} — ${r.createdAt}`)
669
+ : [' None'];
670
+ const sentLines = sent.length > 0
671
+ ? sent.map((r, i) => ` ${i + 1}. To: ${r.receiver?.displayName ?? 'Unknown'} (${r.receiver?.uuid ?? '?'}) — Request ID: ${r.id} — ${r.createdAt}`)
672
+ : [' None'];
673
+ return textResult([
674
+ `Received Friend Requests (${received.length}):`,
675
+ ...receivedLines,
676
+ '',
677
+ `Sent Friend Requests (${sent.length}):`,
678
+ ...sentLines,
679
+ ].join('\n'));
680
+ }
681
+ catch (err) {
682
+ return errorResult(err.message);
683
+ }
684
+ },
685
+ },
613
686
  send_friend_request: {
614
687
  name: 'send_friend_request',
615
688
  description: 'Send a friend request to another user',
@@ -678,6 +751,10 @@ export const TOOL_DEFINITIONS = {
678
751
  if (!betAddress)
679
752
  return errorResult('Missing required parameter: betAddress');
680
753
  try {
754
+ const alreadyFavourited = await client.isBetFavourited(betAddress);
755
+ if (alreadyFavourited) {
756
+ return textResult(`Bet is already in your favourites: ${betAddress}`);
757
+ }
681
758
  await client.favouriteBet(betAddress);
682
759
  return textResult(`Bet added to favourites: ${betAddress}`);
683
760
  }
@@ -702,6 +779,10 @@ export const TOOL_DEFINITIONS = {
702
779
  if (!betAddress)
703
780
  return errorResult('Missing required parameter: betAddress');
704
781
  try {
782
+ const isFavourited = await client.isBetFavourited(betAddress);
783
+ if (!isFavourited) {
784
+ return textResult(`Bet is not in your favourites: ${betAddress}`);
785
+ }
705
786
  await client.unfavouriteBet(betAddress);
706
787
  return textResult(`Bet removed from favourites: ${betAddress}`);
707
788
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solworks/poll-mcp",
3
- "version": "0.1.20",
3
+ "version": "0.1.26",
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",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@modelcontextprotocol/sdk": "^1.12.1",
21
- "@solworks/poll-api-client": "latest",
21
+ "@solworks/poll-api-client": "^0.1.17",
22
22
  "zod": "^3.24.0"
23
23
  },
24
24
  "devDependencies": {
@@ -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 () => {
@@ -138,6 +140,17 @@ describe('MCP Server integration', () => {
138
140
  expect(prompts.length).toBe(3);
139
141
  });
140
142
 
143
+ it('can read a resource and re-read returns fresh data', async () => {
144
+ const result1 = await mcpClient.readResource({ uri: 'poll://user/balances' });
145
+ expect(result1.contents[0].text).toContain('XP');
146
+
147
+ const result2 = await mcpClient.readResource({ uri: 'poll://user/balances' });
148
+ expect(result2.contents[0].text).toContain('XP');
149
+
150
+ // Verify handler was called twice (no server-side caching)
151
+ expect(client.getXpBalance).toHaveBeenCalledTimes(2);
152
+ });
153
+
141
154
  it('handles API errors gracefully', async () => {
142
155
  const errorClient = mockClient({
143
156
  getAccount: vi.fn().mockRejectedValue(new Error('API Error 401: Unauthorized')),
@@ -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
 
@@ -104,6 +106,41 @@ describe('Bet tool handlers', () => {
104
106
  expect(result.content[0].text).toContain('Missing required parameter: betAddress');
105
107
  });
106
108
 
109
+ it('vote returns error when voting not initiated', async () => {
110
+ const client = mockClient({
111
+ getBet: vi.fn().mockResolvedValue({
112
+ status: 'Pending',
113
+ votes: [],
114
+ }),
115
+ getAccount: vi.fn().mockResolvedValue({ uuid: 'user-1' }),
116
+ });
117
+ const result = await TOOL_DEFINITIONS.vote.handler(
118
+ { betAddress: 'addr1', optionIndex: 0 },
119
+ client
120
+ );
121
+ expect(result.isError).toBe(true);
122
+ expect(result.content[0].text).toContain('voting has not been initiated');
123
+ });
124
+
125
+ it('vote succeeds when bet is in Resolving status', async () => {
126
+ const voteFn = vi.fn().mockResolvedValue({});
127
+ const client = mockClient({
128
+ getBet: vi.fn().mockResolvedValue({
129
+ status: 'Resolving',
130
+ votes: [],
131
+ }),
132
+ getAccount: vi.fn().mockResolvedValue({ uuid: 'user-1' }),
133
+ vote: voteFn,
134
+ });
135
+ const result = await TOOL_DEFINITIONS.vote.handler(
136
+ { betAddress: 'addr1', optionIndex: 0 },
137
+ client
138
+ );
139
+ expect(result.isError).toBeUndefined();
140
+ expect(result.content[0].text).toContain('Vote cast successfully');
141
+ expect(voteFn).toHaveBeenCalledWith({ marketPubkey: 'addr1', outcome: 'for' });
142
+ });
143
+
107
144
  it('create_bet handles API error', async () => {
108
145
  const client = mockClient({
109
146
  createBet: vi.fn().mockRejectedValue(new Error('API Error 400: Invalid question')),
@@ -23,7 +23,10 @@ describe('Social tool handlers', () => {
23
23
 
24
24
  it('favourite_bet calls client', async () => {
25
25
  const favouriteFn = vi.fn().mockResolvedValue({});
26
- const client = mockClient({ favouriteBet: favouriteFn });
26
+ const client = mockClient({
27
+ isBetFavourited: vi.fn().mockResolvedValue(false),
28
+ favouriteBet: favouriteFn,
29
+ });
27
30
  const result = await TOOL_DEFINITIONS.favourite_bet.handler(
28
31
  { betAddress: 'addr-fav' },
29
32
  client
@@ -33,6 +36,51 @@ describe('Social tool handlers', () => {
33
36
  expect(favouriteFn).toHaveBeenCalledWith('addr-fav');
34
37
  });
35
38
 
39
+ it('favourite_bet returns already-favourited message', async () => {
40
+ const favouriteFn = vi.fn();
41
+ const client = mockClient({
42
+ isBetFavourited: vi.fn().mockResolvedValue(true),
43
+ favouriteBet: favouriteFn,
44
+ });
45
+ const result = await TOOL_DEFINITIONS.favourite_bet.handler(
46
+ { betAddress: 'addr-fav' },
47
+ client
48
+ );
49
+ expect(result.isError).toBeUndefined();
50
+ expect(result.content[0].text).toContain('Bet is already in your favourites: addr-fav');
51
+ expect(favouriteFn).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('unfavourite_bet removes favourite', async () => {
55
+ const unfavouriteFn = vi.fn().mockResolvedValue({});
56
+ const client = mockClient({
57
+ isBetFavourited: vi.fn().mockResolvedValue(true),
58
+ unfavouriteBet: unfavouriteFn,
59
+ });
60
+ const result = await TOOL_DEFINITIONS.unfavourite_bet.handler(
61
+ { betAddress: 'addr-fav' },
62
+ client
63
+ );
64
+ expect(result.isError).toBeUndefined();
65
+ expect(result.content[0].text).toContain('Bet removed from favourites: addr-fav');
66
+ expect(unfavouriteFn).toHaveBeenCalledWith('addr-fav');
67
+ });
68
+
69
+ it('unfavourite_bet returns not-favourited message', async () => {
70
+ const unfavouriteFn = vi.fn();
71
+ const client = mockClient({
72
+ isBetFavourited: vi.fn().mockResolvedValue(false),
73
+ unfavouriteBet: unfavouriteFn,
74
+ });
75
+ const result = await TOOL_DEFINITIONS.unfavourite_bet.handler(
76
+ { betAddress: 'addr-fav' },
77
+ client
78
+ );
79
+ expect(result.isError).toBeUndefined();
80
+ expect(result.content[0].text).toContain('Bet is not in your favourites: addr-fav');
81
+ expect(unfavouriteFn).not.toHaveBeenCalled();
82
+ });
83
+
36
84
  it('send_friend_request calls client', async () => {
37
85
  const sendFn = vi.fn().mockResolvedValue({});
38
86
  const client = mockClient({ sendFriendRequest: sendFn });