@pnlmarket/mcp-server 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +83 -0
  5. package/dist/install.d.ts +1 -0
  6. package/dist/install.js +168 -0
  7. package/dist/lib/output.d.ts +95 -0
  8. package/dist/lib/output.js +175 -0
  9. package/dist/lib/passphrase.d.ts +16 -0
  10. package/dist/lib/passphrase.js +57 -0
  11. package/dist/lib/pnl-api.d.ts +65 -0
  12. package/dist/lib/pnl-api.js +89 -0
  13. package/dist/lib/sign.d.ts +40 -0
  14. package/dist/lib/sign.js +126 -0
  15. package/dist/lib/wallet.d.ts +74 -0
  16. package/dist/lib/wallet.js +405 -0
  17. package/dist/tools/browse-markets.d.ts +12 -0
  18. package/dist/tools/browse-markets.js +91 -0
  19. package/dist/tools/claim-now.d.ts +10 -0
  20. package/dist/tools/claim-now.js +113 -0
  21. package/dist/tools/claim.d.ts +10 -0
  22. package/dist/tools/claim.js +43 -0
  23. package/dist/tools/export-keypair.d.ts +10 -0
  24. package/dist/tools/export-keypair.js +25 -0
  25. package/dist/tools/get-market.d.ts +10 -0
  26. package/dist/tools/get-market.js +58 -0
  27. package/dist/tools/help.d.ts +7 -0
  28. package/dist/tools/help.js +54 -0
  29. package/dist/tools/init.d.ts +7 -0
  30. package/dist/tools/init.js +69 -0
  31. package/dist/tools/notify.d.ts +12 -0
  32. package/dist/tools/notify.js +150 -0
  33. package/dist/tools/pitch-idea.d.ts +38 -0
  34. package/dist/tools/pitch-idea.js +176 -0
  35. package/dist/tools/pitch-now.d.ts +39 -0
  36. package/dist/tools/pitch-now.js +179 -0
  37. package/dist/tools/restore.d.ts +11 -0
  38. package/dist/tools/restore.js +45 -0
  39. package/dist/tools/set-username.d.ts +10 -0
  40. package/dist/tools/set-username.js +87 -0
  41. package/dist/tools/unlock.d.ts +17 -0
  42. package/dist/tools/unlock.js +47 -0
  43. package/dist/tools/vote-now.d.ts +13 -0
  44. package/dist/tools/vote-now.js +146 -0
  45. package/dist/tools/vote.d.ts +12 -0
  46. package/dist/tools/vote.js +49 -0
  47. package/dist/tools/wallet.d.ts +7 -0
  48. package/dist/tools/wallet.js +40 -0
  49. package/package.json +64 -0
  50. package/skills/README.md +45 -0
  51. package/skills/pnl-browse/SKILL.md +30 -0
  52. package/skills/pnl-claim/SKILL.md +60 -0
  53. package/skills/pnl-claim-now/SKILL.md +67 -0
  54. package/skills/pnl-export/SKILL.md +39 -0
  55. package/skills/pnl-help/SKILL.md +17 -0
  56. package/skills/pnl-init/SKILL.md +24 -0
  57. package/skills/pnl-lock/SKILL.md +17 -0
  58. package/skills/pnl-name/SKILL.md +32 -0
  59. package/skills/pnl-notify/SKILL.md +57 -0
  60. package/skills/pnl-pitch/SKILL.md +83 -0
  61. package/skills/pnl-pitch-now/SKILL.md +88 -0
  62. package/skills/pnl-restore/SKILL.md +38 -0
  63. package/skills/pnl-unlock/SKILL.md +28 -0
  64. package/skills/pnl-vote/SKILL.md +48 -0
  65. package/skills/pnl-vote-now/SKILL.md +68 -0
  66. package/skills/pnl-wallet/SKILL.md +22 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bishwanath Bastola
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # `@pnlmarket/mcp-server`
2
+
3
+ The Model Context Protocol server for [PNL](https://pnl.market) — let
4
+ your agent (Claude Code, Cursor, Cline, Codex, the next one) browse
5
+ live conviction markets on Solana, pitch new ideas as markets, and
6
+ stake YES / NO without leaving the terminal.
7
+
8
+ PNL is a coordination market for ideas. Anyone posts an idea for ~$2
9
+ in SOL; a global market of believers and critics stakes real SOL on
10
+ whether it deserves to launch as a token on pump.fun. YES wins → token
11
+ auto-launches, founder earns royalties. NO wins → critics split the
12
+ pool, paid for filtering noise. The protocol is documented at
13
+ [docs.pnl.market](https://docs.pnl.market) (MIT licensed, open public
14
+ API, manifesto + transparency disclosures published).
15
+
16
+ This MCP server is the bridge from your agent window into that
17
+ market.
18
+
19
+ ## Status
20
+
21
+ **v0.3.0 — autosign live.** 13 tools across read, identity, wallet,
22
+ and market actions.
23
+
24
+ ### Read tools (no auth)
25
+
26
+ | Tool | What it does |
27
+ |---|---|
28
+ | `pnl_help` | Discovery menu — every tool, current wallet state, next step |
29
+ | `pnl_browse_markets` | List live (or historical) conviction markets, filter by status, paginate |
30
+ | `pnl_get_market` | Fetch a single market's full state by id or on-chain address |
31
+
32
+ ### Wallet tools (local, encrypted at rest)
33
+
34
+ | Tool | What it does |
35
+ |---|---|
36
+ | `pnl_init` | First-run: generate BIP39 mnemonic + Ed25519 keypair, encrypt with passphrase, write to `~/.config/pnl/wallet.enc` (mode 0600) |
37
+ | `pnl_wallet` | Address, balance, lock state, autosign cap, active RPC |
38
+ | `pnl_unlock` | Decrypt secret in memory for N min (default 5, max 60). Passphrase via `PNL_PASSPHRASE` env or OS-native dialog — **never** typed in chat |
39
+ | `pnl_lock` | Wipe cached secret immediately |
40
+ | `pnl_restore` | Rebuild wallet on a new machine from a BIP39 mnemonic |
41
+ | `pnl_export_keypair` | Write secret to a 0600 file for password-manager backup (path returned, never the secret itself) |
42
+
43
+ ### Identity
44
+
45
+ | Tool | What it does |
46
+ |---|---|
47
+ | `pnl_set_username` | Claim or rename your PNL username — signature-auth, no Privy session needed |
48
+
49
+ ### Market actions
50
+
51
+ Two flows for each write action:
52
+
53
+ - **Deep-link** (browser signs) — works on any wallet, no MCP unlock needed
54
+ - **Autosign** (MCP signs locally) — for amounts inside the autosign cap, no browser bounce
55
+
56
+ | Tool | Mode | What it does |
57
+ |---|---|---|
58
+ | `pnl_pitch_idea` | Deep-link | Draft a market, return `pnl.market/create?draft=<id>` for the user to confirm + sign in their browser wallet |
59
+ | `pnl_pitch_now` | Autosign | Pin metadata, build + sign the `create_market` tx locally, persist via sig-auth. One shot, no browser |
60
+ | `pnl_vote` | Deep-link | Return `pnl.market/market/<id>?vote=&amount=` deep-link for the user to sign in browser |
61
+ | `pnl_vote_now` | Autosign | Build + sign the `buy_yes` / `buy_no` tx locally for stakes under the cap |
62
+
63
+ ### Trust model
64
+
65
+ The MCP server holds an encrypted keypair on the local disk. The
66
+ passphrase is never delivered through the agent's chat transcript —
67
+ it comes from `PNL_PASSPHRASE` (set in your MCP host config) or an
68
+ OS-native dialog. Cached secrets time out automatically and on
69
+ `pnl_lock`.
70
+
71
+ Autosign is bounded by the **autosign cap** (defaults 0.05 SOL),
72
+ configurable via `pnl_unlock`'s output or by editing
73
+ `~/.config/pnl/config.json`. Per-call override available via
74
+ `autosignCapSol`. Anything above the cap requires the deep-link
75
+ flow — by design.
76
+
77
+ PNL's non-custodial framing in the
78
+ [regulatory posture](https://docs.pnl.market/docs/transparency/regulatory-posture)
79
+ section is load-bearing — the MCP only signs with keys the user
80
+ controls on their own machine.
81
+
82
+ ## Install
83
+
84
+ ### One-shot installer (recommended)
85
+
86
+ After `pnpm install && pnpm -F @pnlmarket/mcp-server build`:
87
+
88
+ ```bash
89
+ npx @pnlmarket/mcp-server install --write
90
+ ```
91
+
92
+ That wires the MCP server into every supported host config it
93
+ finds on the machine (Claude Code, Cursor, Cline, Codex, Windsurf)
94
+ and copies the 13 slash-command skills into
95
+ `~/.claude/skills/`.
96
+
97
+ Run without `--write` first to see the plan. Flags:
98
+
99
+ - `--skills` — only install the slash commands, skip MCP config writes
100
+ - `--no-skills` — skip slash commands, only wire MCP servers
101
+
102
+ ### Manual
103
+
104
+ The compiled entry point is `apps/mcp/dist/index.js`.
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "pnl": {
110
+ "command": "node",
111
+ "args": ["/absolute/path/to/pnl/apps/mcp/dist/index.js"]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## Slash commands (Claude Code)
118
+
119
+ The installer drops these into `~/.claude/skills/`:
120
+
121
+ ```
122
+ /pnl-help /pnl-browse /pnl-name
123
+ /pnl-init /pnl-pitch /pnl-restore
124
+ /pnl-wallet /pnl-pitch-now /pnl-export
125
+ /pnl-unlock /pnl-vote
126
+ /pnl-lock /pnl-vote-now
127
+ ```
128
+
129
+ Each is a small Markdown skill manifest that tells the agent how
130
+ to gather the right context from the conversation and which tool
131
+ to call.
132
+
133
+ ## Environment variables
134
+
135
+ | Variable | Default | When to override |
136
+ |---|---|---|
137
+ | `PNL_API_BASE_URL` | `https://pnl.market` | Pointing at devnet, staging, or a local Next.js dev server |
138
+ | `PNL_RPC_URL` | `https://pnl.market/api/mcp/rpc` (hosted proxy) | BYO Helius key (recommended for heavy use — grab one at [helius.dev](https://helius.dev) and set this to your endpoint) |
139
+ | `PNL_PASSPHRASE` | (prompt via OS dialog) | Skip the OS-native unlock prompt — useful when running an agent unattended, but only set this in your MCP host config, never in shell history |
140
+
141
+ The default RPC is the hosted proxy. It works zero-setup; under
142
+ load it's rate-limited to 60 reads/min and 10 sends/min per IP.
143
+ Set `PNL_RPC_URL` to your own Helius endpoint to bypass that.
144
+
145
+ ## Try it
146
+
147
+ Once wired into your agent:
148
+
149
+ > "What's live on PNL right now?"
150
+
151
+ Triggers `pnl_browse_markets` — active markets with YES%, pool size,
152
+ vote counts, clickable URLs.
153
+
154
+ > "Set me up on PNL"
155
+
156
+ The agent walks `pnl_init` (mnemonic + passphrase), funds-the-wallet
157
+ hint, `pnl_unlock`, `pnl_set_username`.
158
+
159
+ > "Pitch this idea on PNL and sign it for me"
160
+
161
+ If the wallet is unlocked and the creation fee fits the autosign
162
+ cap, the agent calls `pnl_pitch_now` and the market is live in
163
+ ~10s — no browser bounce. Otherwise it falls back to `pnl_pitch_idea`
164
+ (deep-link).
165
+
166
+ > "Stake 0.02 YES on the Nakshatra market"
167
+
168
+ Same shape — `pnl_vote_now` if within cap, `pnl_vote` if not.
169
+
170
+ ## Wallet file layout
171
+
172
+ ```
173
+ ~/.config/pnl/
174
+ ├── wallet.enc # encrypted secret + metadata (mode 0600)
175
+ ├── config.json # autosign cap + RPC URL (mode 0644)
176
+ └── exports/ # timestamped backup dumps (mode 0700)
177
+ ```
178
+
179
+ Crypto: scrypt (N=2^17, r=8, p=1) → AES-256-GCM. BIP39 12-word
180
+ mnemonic at the Phantom-compatible derivation path
181
+ `m/44'/501'/0'/0'` so backups import cleanly into
182
+ Phantom / Solflare / Backpack and vice versa.
183
+
184
+ ## Backend surface
185
+
186
+ The MCP autosign flows talk to four sig-auth-aware endpoints on
187
+ pnl.market:
188
+
189
+ ```
190
+ POST /api/mcp/rpc JSON-RPC proxy → Helius
191
+ POST /api/mcp/markets/build-create-tx unsigned create_market tx
192
+ POST /api/mcp/markets/complete-create sig-auth → persist Project + Market
193
+ POST /api/mcp/markets/build-vote-tx unsigned buy_yes / buy_no tx
194
+ POST /api/mcp/markets/complete-vote sig-auth → persist TradeHistory + counts
195
+ POST /api/mcp/profile sig-auth → username claim/rename
196
+ ```
197
+
198
+ The challenge format is canonical:
199
+ `pnl-mcp:<kind>:<fingerprint>:<nonce>` where `<kind>` is one of
200
+ `complete-create | complete-vote | profile`, `<fingerprint>` is
201
+ either the tx signature (for complete-*) or the username (for
202
+ profile), and `<nonce>` is `<unix-ms>-<hex>` with a 5min freshness
203
+ window + 1min clock-skew tolerance. The MCP signs with the local
204
+ keypair; the backend verifies the signature is good for the
205
+ claimed wallet via tweetnacl.
206
+
207
+ ## Links
208
+
209
+ - Live: [pnl.market](https://pnl.market)
210
+ - Docs: [docs.pnl.market](https://docs.pnl.market) (manifesto + transparency + public API reference)
211
+ - Repo: [github.com/aitankfish/pnl](https://github.com/aitankfish/pnl)
212
+ - Manifesto: [docs.pnl.market/docs/manifesto](https://docs.pnl.market/docs/manifesto)
213
+
214
+ ## License
215
+
216
+ MIT.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // @pnlmarket/mcp-server — Model Context Protocol server for PNL.
3
+ //
4
+ // Lets agents (Claude Code, Cursor, Cline, Codex, the next one) browse
5
+ // the live conviction-market state on pnl.market and — in later versions
6
+ // — prepare new pitches and votes the user signs in their own wallet.
7
+ //
8
+ // Critical: this server NEVER holds keys. Read tools are pure fetches
9
+ // against the public API. Write-prep tools (coming next) return deep-
10
+ // links to pnl.market; the user always confirms the signature in their
11
+ // own wallet.
12
+ //
13
+ // Transport: stdio. That's the standard for Claude Code / Cursor / Cline.
14
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { browseMarketsInputSchema, callBrowseMarkets } from './tools/browse-markets.js';
17
+ import { getMarketInputSchema, callGetMarket } from './tools/get-market.js';
18
+ import { initInputSchema, callInit } from './tools/init.js';
19
+ import { walletInputSchema, callWallet } from './tools/wallet.js';
20
+ import { exportKeypairInputSchema, callExportKeypair } from './tools/export-keypair.js';
21
+ import { pitchIdeaInputSchema, callPitchIdea } from './tools/pitch-idea.js';
22
+ import { setUsernameInputSchema, callSetUsername } from './tools/set-username.js';
23
+ import { unlockInputSchema, callUnlock, lockInputSchema, callLock } from './tools/unlock.js';
24
+ import { restoreInputSchema, callRestore } from './tools/restore.js';
25
+ import { helpInputSchema, callHelp } from './tools/help.js';
26
+ import { voteInputSchema, callVote } from './tools/vote.js';
27
+ import { pitchNowInputSchema, callPitchNow } from './tools/pitch-now.js';
28
+ import { voteNowInputSchema, callVoteNow } from './tools/vote-now.js';
29
+ import { claimInputSchema, callClaim } from './tools/claim.js';
30
+ import { claimNowInputSchema, callClaimNow } from './tools/claim-now.js';
31
+ import { notifyInputSchema, callNotify } from './tools/notify.js';
32
+ import { runInstall } from './install.js';
33
+ const SERVER_NAME = 'pnl-mcp-server';
34
+ const SERVER_VERSION = '0.4.0';
35
+ // CLI dispatch — when invoked as `pnl-mcp-server install`, run the
36
+ // installer that wires this server into the user's agent configs and
37
+ // drops the slash-command skills into ~/.claude/skills/. Otherwise
38
+ // (the case the agent runtime hits) start the stdio MCP server.
39
+ async function maybeRunCli() {
40
+ const subcommand = process.argv[2];
41
+ if (subcommand === 'install') {
42
+ const code = await runInstall(process.argv.slice(2));
43
+ process.exit(code);
44
+ }
45
+ return false;
46
+ }
47
+ async function main() {
48
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
49
+ // We only register tools today. Resources / prompts come later
50
+ // alongside the write-prep tools.
51
+ capabilities: { tools: {} },
52
+ });
53
+ server.tool('pnl_help', "Show the PNL command reference with every available tool, what it does, and a typical first-run flow. Context-aware — adjusts the suggested next step based on whether the user has a wallet and whether it's unlocked. Use this when the user types '/pnl-help', says 'how do I use PNL', or asks what's possible.", helpInputSchema, async (args) => callHelp(args));
54
+ server.tool('pnl_browse_markets', "List live conviction markets on PNL. Use this when the user asks 'what's on PNL right now?' or wants to see active idea markets. Returns market names, current YES%, total pool, vote counts, and a URL for each. Filter by status (active, yesWins, noWins, expired, refund, all). Paginate with page+limit.", browseMarketsInputSchema, async (args) => callBrowseMarkets(args));
55
+ server.tool('pnl_get_market', 'Fetch one market by id (from pnl_browse_markets) or by on-chain market address. Returns the full market state — name, founder, description, YES%, pool sizes, expiry, on-chain address. Use this after pnl_browse_markets when the user wants details on a specific market.', getMarketInputSchema, async (args) => callGetMarket(args));
56
+ server.tool('pnl_init', 'First-run setup. Generates a local Solana keypair on this machine (stored at ~/.config/pnl/keypair.json, mode 0600) and returns the deposit address. Call this when the user wants to set up PNL on a new machine or asks "how do I get started with PNL?". Idempotent — if a keypair already exists, returns the existing wallet info.', initInputSchema, async (args) => callInit(args));
57
+ server.tool('pnl_wallet', "Show the local PNL wallet's address and current SOL balance. Read-only. Call this any time the user asks 'what's my PNL wallet?', 'how much SOL do I have?', or to check whether their funding transaction has landed.", walletInputSchema, async (args) => callWallet(args));
58
+ server.tool('pnl_export_keypair', "Reveal the local PNL secret key in base58 (Phantom-import format) and JSON-array (Solana CLI format). Requires confirm: 'EXPORT' to prevent accidental disclosure. Use only when the user explicitly asks to back up their key, move their wallet to Phantom/Solflare/etc., or migrate to another machine.", exportKeypairInputSchema, async (args) => callExportKeypair(args));
59
+ server.tool('pnl_pitch_idea', "Pitch a new idea to PNL as a conviction market. The agent supplies the name, description, ticker symbol, category/type/stage, team size, target pool in SOL, duration in days, and optional provenance (the conversation excerpt + code snippet that birthed the idea). Returns a /create?draft=<id> deep-link the user opens in their browser to confirm + sign the create_market transaction in their own wallet. v0.2 is deep-link only -- v0.3 will add local autosigning for under-cap transactions. Use this when the user says 'pitch this on PNL', 'plant this idea', or similar.", pitchIdeaInputSchema, async (args) => callPitchIdea(args));
60
+ server.tool('pnl_unlock', "Unlock the local PNL wallet for signing. Pulls the passphrase from the PNL_PASSPHRASE env var (set in Claude Code mcp config) or pops an OS-native dialog (osascript on macOS, zenity on Linux). The passphrase NEVER comes from tool arguments and never enters the chat transcript. Caches the unlocked secret in memory for ttl_minutes (default 5, max 60). Re-call to refresh the TTL. Call this before any signing tool: pnl_set_username, pnl_export_keypair, future write-prep tools.", unlockInputSchema, async (args) => callUnlock(args));
61
+ server.tool('pnl_lock', 'Lock the local PNL wallet immediately. Wipes the cached secret from memory, future signing operations require a fresh pnl_unlock. Use this when stepping away from the machine or after a sensitive session.', lockInputSchema, async (args) => callLock(args));
62
+ server.tool('pnl_restore', "Restore a PNL wallet on this machine from a BIP39 mnemonic (12 or 24 words). Use when setting up PNL on a new machine and the user already has the recovery phrase from a previous pnl_init. The mnemonic is the standard format Phantom / Solflare / Backpack / Solana CLI all accept. Refuses to overwrite an existing wallet unless allowOverwrite: true is passed. Passphrase is read from PNL_PASSPHRASE env or via OS dialog.", restoreInputSchema, async (args) => callRestore(args));
63
+ server.tool('pnl_vote', "Stake YES or NO on an existing PNL market. Returns a deep-link URL with the side + amount pre-filled — the user opens it in their browser, confirms the vote panel (already populated), and signs the buy_yes / buy_no transaction with their wallet. Use this when the user says 'vote yes on X', 'fade Y', 'back the AutoImport CLI market', or similar. Phase B will add a local-signing variant for stakes under the autosign cap.", voteInputSchema, async (args) => callVote(args));
64
+ server.tool('pnl_pitch_now', "Autosign create_market. Same payload as pnl_pitch_idea (name, description, ticker, category, type, stage, team size, target pool, duration), but the MCP signs the create_market transaction locally with the encrypted-at-rest keypair and persists the market without bouncing through the browser. The wallet must be unlocked (pnl_unlock) and the creation fee (~0.015 SOL) must be within the autosign cap (defaults 0.05 SOL, overridable per-call via autosignCapSol). Returns the live market URL + tx signature. Use this when the user says 'pitch this on PNL and sign it for me', 'auto-create the market', or after pnl_pitch_idea when they say 'just do it'.", pitchNowInputSchema, async (args) => callPitchNow(args));
65
+ server.tool('pnl_vote_now', "Autosign buy_yes / buy_no. Same shape as pnl_vote (marketId + side + amountSol) but the MCP signs the vote transaction locally and persists the trade without bouncing through the browser. Requires an unlocked wallet and the stake to be within the autosign cap (defaults 0.05 SOL, overridable per-call). Returns the on-chain tx signature + Solscan link. Use this when the user says 'vote yes on X for me', 'stake 0.02 NO on the AutoImport market', or similar autonomous-vote intent. For larger stakes, fall back to pnl_vote (deep-link).", voteNowInputSchema, async (args) => callVoteNow(args));
66
+ server.tool('pnl_claim', "Claim rewards on a resolved PNL market in deep-link mode. Returns a /market/<id>?claim=1 URL — the market detail page opens the claim panel and the user signs the claim_rewards tx in their browser wallet. The market must be resolved (YES wins, NO wins, or refund) and the wallet must have an unclaimed position on it. For autosign (no browser), use pnl_claim_now. Use when the user says 'claim my rewards on X', 'claim YES wins on AutoImport', or after a market the user voted on has resolved.", claimInputSchema, async (args) => callClaim(args));
67
+ server.tool('pnl_claim_now', "Autosign claim_rewards. Same arg as pnl_claim (marketId). The MCP fetches the on-chain market + position state, builds the claim_rewards tx (with Token2022 ATA creation for YES wins), signs locally with the encrypted-at-rest keypair, sends, and persists. No autosign cap — claiming is a withdrawal of funds the user is already owed, not a spend. Wallet must be unlocked (pnl_unlock). Returns the on-chain tx + Solscan link + profile URL. Use when the user says 'claim it for me' or 'auto-claim X' on a resolved market.", claimNowInputSchema, async (args) => callClaimNow(args));
68
+ server.tool('pnl_notify', "Show recent notifications for the local PNL wallet — votes on markets you created, resolutions, claim-ready alerts, milestones. Stateful: tracks last-seen on disk so successive calls only show new items (pass all: true to override). Includes a clickable link to the user's PNL profile at /profile/<wallet>. Use when the user says 'what's new on PNL', 'any updates', 'check notifications', or invokes /pnl-notify. No wallet unlock needed (read-only).", notifyInputSchema, async (args) => callNotify(args));
69
+ server.tool('pnl_set_username', "Claim or rename the PNL username for the local wallet. Signs a time-bounded challenge with the keypair from pnl_init so the backend can verify wallet ownership -- no Privy session or Gmail login required. Usernames are 3-20 characters of letters/numbers/_/-. Returns 'taken' if another wallet has claimed the name. Use when the user says 'set my PNL username to X', 'rename my PNL profile', or after pnl_init when they want a custom name instead of the auto-generated Cosmic one.", setUsernameInputSchema, async (args) => callSetUsername(args));
70
+ const transport = new StdioServerTransport();
71
+ await server.connect(transport);
72
+ // Stdio transport keeps the process alive while the host (Claude Code /
73
+ // Cursor / etc.) holds the pipe. No need to log anything to stdout —
74
+ // stdout is the MCP message channel. stderr is fine for diagnostics.
75
+ process.stderr.write(`[${SERVER_NAME}@${SERVER_VERSION}] ready · ${process.env.PNL_API_BASE_URL || 'https://pnl.market'}\n`);
76
+ }
77
+ (async () => {
78
+ await maybeRunCli();
79
+ await main();
80
+ })().catch((err) => {
81
+ process.stderr.write(`[${SERVER_NAME}] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
82
+ process.exit(1);
83
+ });
@@ -0,0 +1 @@
1
+ export declare function runInstall(argv: string[]): Promise<number>;
@@ -0,0 +1,168 @@
1
+ // ─── pnl-mcp-server install ──────────────────────────────────────
2
+ //
3
+ // One-shot installer that wires @pnlmarket/mcp-server into the user's agent
4
+ // of choice (Claude Code, Cursor, Cline) and optionally drops the
5
+ // SKILL.md slash commands into their skills directory.
6
+ //
7
+ // Usage:
8
+ // npx @pnlmarket/mcp-server install # prints plan, asks for confirm
9
+ // npx @pnlmarket/mcp-server install --write # apply without confirmation
10
+ // npx @pnlmarket/mcp-server install --skills # only install slash commands
11
+ //
12
+ // Idempotent: re-running is safe. Existing entries get replaced
13
+ // in-place, no duplicates accumulate.
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { join, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ const SERVER_KEY = 'pnl';
19
+ const NPX_COMMAND = 'npx';
20
+ const NPX_ARGS = ['-y', '@pnlmarket/mcp-server'];
21
+ function candidates() {
22
+ const home = homedir();
23
+ return [
24
+ {
25
+ label: 'Claude Code CLI',
26
+ configPath: join(home, '.claude.json'),
27
+ description: 'The Claude Code terminal app reads MCP servers from here.',
28
+ },
29
+ {
30
+ label: 'Claude Desktop (macOS)',
31
+ configPath: join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
32
+ description: 'The desktop app on macOS.',
33
+ },
34
+ {
35
+ label: 'Cursor',
36
+ configPath: join(home, '.cursor', 'mcp.json'),
37
+ description: 'Cursor MCP config.',
38
+ },
39
+ ];
40
+ }
41
+ function readJson(path) {
42
+ try {
43
+ return JSON.parse(readFileSync(path, 'utf8'));
44
+ }
45
+ catch {
46
+ return {};
47
+ }
48
+ }
49
+ function writeJson(path, data) {
50
+ mkdirSync(dirname(path), { recursive: true });
51
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
52
+ }
53
+ function ensureMcpServer(config) {
54
+ const existing = config.mcpServers ?? {};
55
+ const current = existing[SERVER_KEY];
56
+ const matches = current?.command === NPX_COMMAND &&
57
+ Array.isArray(current.args) &&
58
+ current.args.length === NPX_ARGS.length &&
59
+ current.args.every((a, i) => a === NPX_ARGS[i]);
60
+ if (matches) {
61
+ return { changed: false, existingMatches: true };
62
+ }
63
+ config.mcpServers = {
64
+ ...existing,
65
+ [SERVER_KEY]: { command: NPX_COMMAND, args: NPX_ARGS },
66
+ };
67
+ return { changed: true, existingMatches: false };
68
+ }
69
+ function installSkills() {
70
+ const targetDir = join(homedir(), '.claude', 'skills');
71
+ mkdirSync(targetDir, { recursive: true });
72
+ // The skills directory ships alongside the npm tarball. From dist/,
73
+ // that's ../skills.
74
+ const here = dirname(fileURLToPath(import.meta.url));
75
+ const skillsRoot = join(here, '..', 'skills');
76
+ if (!existsSync(skillsRoot)) {
77
+ return { copied: 0, targetDir };
78
+ }
79
+ let copied = 0;
80
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
81
+ if (!entry.isDirectory() || !entry.name.startsWith('pnl-'))
82
+ continue;
83
+ const src = join(skillsRoot, entry.name);
84
+ const dest = join(targetDir, entry.name);
85
+ // cpSync with recursive overwrite — re-running the installer just
86
+ // refreshes the templates with whatever the current package ships.
87
+ cpSync(src, dest, { recursive: true, force: true });
88
+ copied++;
89
+ }
90
+ return { copied, targetDir };
91
+ }
92
+ function logBlock(title, body) {
93
+ process.stdout.write(`\n── ${title} ──\n${body}\n`);
94
+ }
95
+ export async function runInstall(argv) {
96
+ const flags = new Set(argv.slice(1));
97
+ const write = flags.has('--write') || flags.has('-y') || flags.has('--yes');
98
+ const skillsOnly = flags.has('--skills');
99
+ const noSkills = flags.has('--no-skills');
100
+ process.stdout.write('\n@pnlmarket/mcp-server installer\n');
101
+ // 1. Discover target configs.
102
+ const targets = candidates();
103
+ const present = targets.filter((t) => existsSync(t.configPath));
104
+ const missing = targets.filter((t) => !existsSync(t.configPath));
105
+ if (skillsOnly) {
106
+ const { copied, targetDir } = installSkills();
107
+ process.stdout.write(`\nSlash commands installed: ${copied} skill${copied === 1 ? '' : 's'} -> ${targetDir}\n`);
108
+ process.stdout.write('Restart Claude Code, then type /pnl-init to begin.\n');
109
+ return 0;
110
+ }
111
+ const planLines = [];
112
+ if (present.length === 0) {
113
+ planLines.push('No agent config files found at the standard locations.');
114
+ planLines.push('You can still run the server directly. After installing globally with `npm i -g @pnlmarket/mcp-server`, add this to your agent\'s MCP config:');
115
+ planLines.push('');
116
+ planLines.push(JSON.stringify({ mcpServers: { [SERVER_KEY]: { command: NPX_COMMAND, args: NPX_ARGS } } }, null, 2));
117
+ }
118
+ else {
119
+ planLines.push('Will add an `mcpServers.pnl` entry pointing to `npx -y @pnlmarket/mcp-server` in:');
120
+ for (const t of present) {
121
+ planLines.push(` • ${t.label}: ${t.configPath}`);
122
+ }
123
+ if (missing.length) {
124
+ planLines.push('');
125
+ planLines.push('Not found (skipped):');
126
+ for (const t of missing) {
127
+ planLines.push(` • ${t.label}: ${t.configPath}`);
128
+ }
129
+ }
130
+ }
131
+ if (!noSkills) {
132
+ planLines.push('');
133
+ planLines.push(`Will also copy the slash-command skills (pnl-init, pnl-wallet, pnl-unlock, pnl-lock, pnl-restore, pnl-export, pnl-name, pnl-browse, pnl-pitch, pnl-pitch-now, pnl-vote, pnl-vote-now, pnl-claim, pnl-claim-now, pnl-notify, pnl-help) to ~/.claude/skills/`);
134
+ }
135
+ logBlock('Plan', planLines.join('\n'));
136
+ if (!write) {
137
+ process.stdout.write('\nRe-run with `--write` (or `-y`) to apply.\n npx @pnlmarket/mcp-server install --write\n\n');
138
+ return 0;
139
+ }
140
+ // 2. Apply.
141
+ let mcpWritten = 0;
142
+ let mcpAlready = 0;
143
+ for (const target of present) {
144
+ const config = readJson(target.configPath);
145
+ const { changed, existingMatches } = ensureMcpServer(config);
146
+ if (existingMatches) {
147
+ mcpAlready++;
148
+ continue;
149
+ }
150
+ if (changed) {
151
+ writeJson(target.configPath, config);
152
+ mcpWritten++;
153
+ }
154
+ }
155
+ const skillsResult = !noSkills ? installSkills() : null;
156
+ logBlock('Done', [
157
+ `MCP server entries written: ${mcpWritten}${mcpAlready ? ` (${mcpAlready} already current)` : ''}`,
158
+ skillsResult
159
+ ? `Slash commands installed: ${skillsResult.copied} skill${skillsResult.copied === 1 ? '' : 's'} -> ${skillsResult.targetDir}`
160
+ : 'Slash commands: skipped (--no-skills)',
161
+ '',
162
+ 'Next:',
163
+ ' 1. Restart Claude Code / Cursor.',
164
+ ' 2. Type /pnl-init in any session — it generates a local Solana keypair and shows the deposit address.',
165
+ ' 3. Fund the wallet with at least 0.05 SOL from any Solana wallet, then /pnl-pitch an idea.',
166
+ ].join('\n'));
167
+ return 0;
168
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shorten a base58 pubkey for display. Keeps the first 6 and last 4
3
+ * characters — enough to be visually distinguishable without taking
4
+ * a full line.
5
+ *
6
+ * truncAddress("9ot5o7tbtUit8j75ivdjxoUCGaY7uCcUDootdKuVhECH")
7
+ * // → "9ot5o7…hECH"
8
+ */
9
+ export declare function truncAddress(addr: string, leading?: number, trailing?: number): string;
10
+ /**
11
+ * Format SOL from lamports with reasonable precision. Returns null
12
+ * when the amount is below display threshold so callers can omit
13
+ * the row entirely.
14
+ */
15
+ export declare function formatSol(lamports: number | string | null | undefined): string | null;
16
+ export declare const Badge: {
17
+ readonly ok: "[ok]";
18
+ readonly warn: "[!]";
19
+ readonly err: "[err]";
20
+ readonly locked: "[locked]";
21
+ readonly unlocked: "[unlocked]";
22
+ readonly live: "[live]";
23
+ readonly ended: "[ended]";
24
+ readonly pending: "[pending]";
25
+ readonly draft: "[draft]";
26
+ };
27
+ /**
28
+ * Render a one-line bold "headline" answer. The user can read just
29
+ * this and stop. Subsequent text is "if you want details".
30
+ */
31
+ export declare function headline(text: string): string;
32
+ /**
33
+ * Two-column key/value table. Used by pnl_wallet, pnl_get_market,
34
+ * etc. for structured "here's what we know" responses.
35
+ */
36
+ export declare function kvTable(rows: Array<[string, string | null | undefined]>): string;
37
+ /**
38
+ * Generic markdown table with headers + rows. Pads cells with one
39
+ * space on each side so the source reads cleanly in case the user
40
+ * is on a renderer that doesn't pretty-print markdown tables.
41
+ */
42
+ export declare function table(headers: string[], rows: string[][]): string;
43
+ /**
44
+ * Code block — useful for pubkeys, tx signatures, IPFS CIDs,
45
+ * mnemonics, file paths. Claude Code renders these as monospace
46
+ * with a copy button.
47
+ */
48
+ export declare function code(content: string, lang?: string): string;
49
+ /**
50
+ * Inline code span — for short identifiers in the middle of a
51
+ * sentence.
52
+ */
53
+ export declare function inline(content: string): string;
54
+ /**
55
+ * Blockquote — used for market descriptions, manifesto-style
56
+ * pull-quotes, important warnings.
57
+ */
58
+ export declare function quote(text: string): string;
59
+ /**
60
+ * Markdown heading — H3 by default since tool output isn't a doc.
61
+ */
62
+ export declare function heading(text: string, level?: 1 | 2 | 3 | 4): string;
63
+ /**
64
+ * Horizontal rule. Use sparingly to separate big sections.
65
+ */
66
+ export declare const hr = "---";
67
+ /**
68
+ * Action hint at the bottom of an output. Standardized so users
69
+ * recognize the pattern.
70
+ */
71
+ export declare function next(hint: string): string;
72
+ /**
73
+ * Wrap a tool result in the MCP content-block shape so callers
74
+ * don't have to repeat the boilerplate. Joins parts with two
75
+ * newlines so each section gets a paragraph break.
76
+ */
77
+ export declare function reply(...parts: Array<string | null | undefined | false>): {
78
+ content: Array<{
79
+ type: 'text';
80
+ text: string;
81
+ }>;
82
+ };
83
+ /**
84
+ * Render a Solana address: truncated for the eye, plus a code block
85
+ * with the full thing for copy. Optionally include a Solscan link.
86
+ */
87
+ export declare function addressBlock(addr: string, opts?: {
88
+ label?: string;
89
+ solscan?: boolean;
90
+ }): string;
91
+ /**
92
+ * Render a market URL with its truncated id and the full URL on a
93
+ * separate line for copy.
94
+ */
95
+ export declare function marketLink(marketId: string, baseUrl: string): string;