Repository navigation
feat: new design for mcp page #1353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70a0528
2e6261e
2968f20
90c3d29
bd8aec9
8cf8bd0
d77875d
de74974
679c4ec
9b58393
8bff052
bd7e501
19b23bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { describe, expect, test } from 'vitest'; | ||
|
|
||
| import { type AnswerBlock, answerCost, MCP_EXAMPLES, revealAnswer } from '../example-answers'; | ||
|
|
||
| const BLOCKS: AnswerBlock[] = [ | ||
| { kind: 'text', text: 'abcdefghij' }, | ||
| { | ||
| head: ['A', 'B'], | ||
| kind: 'table', | ||
| rows: [ | ||
| ['1', '2'], | ||
| ['3', '4'], | ||
| ['5', '6'], | ||
| ], | ||
| }, | ||
| { kind: 'text', text: 'tail' }, | ||
| ]; | ||
|
|
||
| describe('revealAnswer', () => { | ||
| test('should show nothing before the first character', () => { | ||
| expect(revealAnswer(BLOCKS, 0)).toEqual([]); | ||
| }); | ||
|
|
||
| test('should type the first block one character at a time and stop there', () => { | ||
| const shown = revealAnswer(BLOCKS, 4); | ||
| expect(shown).toHaveLength(1); | ||
| expect(shown[0]).toMatchObject({ partial: true, text: 'abcd' }); | ||
| }); | ||
|
|
||
| test('should open the table on its frame, then fill rows in', () => { | ||
| const beforeTable = 10; | ||
| const frameOnly = revealAnswer(BLOCKS, beforeTable + 1); | ||
| expect(frameOnly).toHaveLength(2); | ||
| expect(frameOnly[1]).toMatchObject({ partial: true, rows: 0 }); | ||
|
|
||
| // 24 for the frame, then 26 per row. | ||
| expect(revealAnswer(BLOCKS, beforeTable + 24 + 26)[1]).toMatchObject({ partial: true, rows: 1 }); | ||
| expect(revealAnswer(BLOCKS, beforeTable + 24 + 26 * 2)[1]).toMatchObject({ partial: true, rows: 2 }); | ||
| }); | ||
|
|
||
| test('should never reveal a later block before an earlier one is complete', () => { | ||
| for (let revealed = 0; revealed <= answerCost(BLOCKS); revealed++) { | ||
| const shown = revealAnswer(BLOCKS, revealed); | ||
| const partials = shown.filter(entry => entry.partial); | ||
| expect(partials.length).toBeLessThanOrEqual(1); | ||
| // Only the last visible block may be mid-typing. | ||
| if (partials.length === 1) expect(shown.at(-1)?.partial).toBe(true); | ||
| } | ||
| }); | ||
|
|
||
| test('should reveal every block completely at the full cost', () => { | ||
| const shown = revealAnswer(BLOCKS, answerCost(BLOCKS)); | ||
| expect(shown).toHaveLength(BLOCKS.length); | ||
| expect(shown.every(entry => !entry.partial)).toBe(true); | ||
| expect(shown[0].text).toBe('abcdefghij'); | ||
| expect(shown[1].rows).toBe(3); | ||
| expect(shown[2].text).toBe('tail'); | ||
| }); | ||
|
|
||
| test('should grow monotonically — text never shrinks, rows never go backwards', () => { | ||
| let lastText = 0; | ||
| let lastRows = 0; | ||
| for (let revealed = 0; revealed <= answerCost(BLOCKS); revealed++) { | ||
| const shown = revealAnswer(BLOCKS, revealed); | ||
| const text = shown.reduce((sum, entry) => sum + entry.text.length, 0); | ||
| const rows = shown.reduce((sum, entry) => sum + entry.rows, 0); | ||
| expect(text).toBeGreaterThanOrEqual(lastText); | ||
| expect(rows).toBeGreaterThanOrEqual(lastRows); | ||
| lastText = text; | ||
| lastRows = rows; | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe('MCP_EXAMPLES', () => { | ||
| test('should play every example back to its complete answer', () => { | ||
| for (const example of MCP_EXAMPLES) { | ||
| const shown = revealAnswer(example.answer, answerCost(example.answer)); | ||
| expect(shown).toHaveLength(example.answer.length); | ||
| expect(shown.every(entry => !entry.partial)).toBe(true); | ||
| } | ||
| }); | ||
|
|
||
| test('should keep ids unique and the short prompt no longer than the real question', () => { | ||
| expect(new Set(MCP_EXAMPLES.map(example => example.id)).size).toBe(MCP_EXAMPLES.length); | ||
| for (const example of MCP_EXAMPLES) { | ||
| expect(example.prompt.length).toBeLessThanOrEqual(example.question.length); | ||
| } | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| export type CtaFieldTuning = { | ||
| density: number; | ||
|
|
||
| drift: number; | ||
|
|
||
| falloff: number; | ||
|
|
||
| flight: number; | ||
|
|
||
| maxSpeed: number; | ||
|
|
||
| pullDensity: number; | ||
|
|
||
| softening: number; | ||
|
|
||
| spawn: number; | ||
| }; | ||
|
|
||
| export const CTA_FIELD_TUNING_DEFAULTS: CtaFieldTuning = { | ||
| density: 1, | ||
| drift: 360, | ||
| falloff: 2, | ||
| flight: 1, | ||
| maxSpeed: 1800, | ||
| pullDensity: 5, | ||
| softening: 24, | ||
| spawn: 1100, | ||
| }; | ||
|
|
||
| export type CtaFieldScope = 'base' | 'closingDesktop' | 'heroDesktop'; | ||
|
|
||
| const CTA_FIELD_SCOPE_DEFAULTS: Record<Exclude<CtaFieldScope, 'base'>, CtaFieldTuning> = { | ||
| closingDesktop: { | ||
| density: 1, | ||
| drift: 360, | ||
| falloff: 2, | ||
| flight: 0.55, | ||
| maxSpeed: 960, | ||
| pullDensity: 2.5, | ||
| softening: 24, | ||
| spawn: 1100, | ||
| }, | ||
| heroDesktop: { | ||
| density: 0.6, | ||
| drift: 320, | ||
| falloff: 1.3, | ||
| flight: 0.3, | ||
| maxSpeed: 360, | ||
| pullDensity: 1.5, | ||
| softening: 24, | ||
| spawn: 550, | ||
| }, | ||
| }; | ||
|
|
||
| export function resolveCtaField(scope: CtaFieldScope): CtaFieldTuning { | ||
| return scope === 'base' | ||
| ? CTA_FIELD_TUNING_DEFAULTS | ||
| : { ...CTA_FIELD_TUNING_DEFAULTS, ...CTA_FIELD_SCOPE_DEFAULTS[scope] }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| export type AnswerBlock = { kind: 'text'; text: string } | { kind: 'table'; head: string[]; rows: string[][] }; | ||
|
|
||
| export type McpExample = { | ||
| id: string; | ||
| prompt: string; | ||
| question: string; | ||
| answer: AnswerBlock[]; | ||
| tool: string; | ||
| }; | ||
|
|
||
| const TABLE_HEAD_COST = 24; | ||
| const TABLE_ROW_COST = 26; | ||
|
|
||
| export type RevealedBlock = { block: AnswerBlock; rows: number; text: string; partial: boolean }; | ||
|
|
||
| export function blockCost(block: AnswerBlock): number { | ||
| return block.kind === 'text' ? block.text.length : TABLE_HEAD_COST + block.rows.length * TABLE_ROW_COST; | ||
| } | ||
|
|
||
| export function answerCost(blocks: AnswerBlock[]): number { | ||
| return blocks.reduce((sum, block) => sum + blockCost(block), 0); | ||
| } | ||
|
|
||
| export function revealAnswer(blocks: AnswerBlock[], revealed: number): RevealedBlock[] { | ||
| const shown: RevealedBlock[] = []; | ||
| let consumed = 0; | ||
|
|
||
| for (const block of blocks) { | ||
| const cost = blockCost(block); | ||
| const into = revealed - consumed; | ||
| if (into <= 0) break; | ||
|
|
||
| const partial = into < cost; | ||
| shown.push({ | ||
| block, | ||
| partial, | ||
| rows: | ||
| block.kind === 'table' | ||
| ? partial | ||
| ? Math.max(0, Math.floor((into - TABLE_HEAD_COST) / TABLE_ROW_COST)) | ||
| : block.rows.length | ||
| : 0, | ||
| text: block.kind === 'text' ? (partial ? block.text.slice(0, Math.floor(into)) : block.text) : '', | ||
| }); | ||
|
|
||
| if (partial) break; | ||
| consumed += cost; | ||
| } | ||
|
|
||
| return shown; | ||
| } | ||
|
|
||
| export const MCP_EXAMPLES: McpExample[] = [ | ||
| { | ||
| answer: [ | ||
| { | ||
| kind: 'text', | ||
| text: '"Sent from my Pumpfun App" (App) — Token-2022 mint, 6 decimals, supply 912,905,455.031061, fixed. Only two extensions, and nobody can change either of them:', | ||
| }, | ||
| { | ||
| head: ['Extension', 'Current state', 'Who can change it'], | ||
| kind: 'table', | ||
| rows: [ | ||
| ['metadataPointer', 'points at the mint itself', 'authority: null — nobody'], | ||
| ['tokenMetadata', 'name, symbol, uri; no extra fields', 'updateAuthority: null — nobody'], | ||
| ], | ||
| }, | ||
| { | ||
| kind: 'text', | ||
| text: 'Mint and freeze authorities are null too, and the extensions that carry real issuer power — permanentDelegate, transferHook, transferFeeConfig, confidential transfers — are simply absent.', | ||
|
Comment on lines
+64
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Token-2022 answer says nobody can change the metadata and lists absent issuer powers, but no longer explains that control of the referenced off-chain metadata domain can still change the displayed name, image, and description. Restoring that distinction would keep readers from mistaking renounced on-chain authority for immutable presentation metadata. Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| }, | ||
| { | ||
| kind: 'text', | ||
| text: 'One caveat: the on-chain authority is renounced, but the tokenMetadata uri points off-chain — whoever controls that domain can still change the name, image, and description a wallet displays.', | ||
| }, | ||
| ], | ||
| id: 'token-2022', | ||
| prompt: 'Which Token-2022 extensions are enabled on 49nkLrXi…3kTa8pTL, and who can still change them?', | ||
| question: | ||
| 'Which Token-2022 extensions are enabled on 49nkLrXi8nCZBVKsShDNasEtPe4Vn1mx9Xbr3kTa8pTL, and who can still change them?', | ||
| tool: 'inspect_entity', | ||
| }, | ||
| { | ||
| answer: [ | ||
| { | ||
| kind: 'text', | ||
| text: 'Squads Multisig Program (SQDS4ep65T…) — BPF upgradeable-loader program on mainnet-beta.', | ||
| }, | ||
| { | ||
| head: ['Field', 'Value'], | ||
| kind: 'table', | ||
| rows: [ | ||
| ['Upgrade authority', 'null — revoked, so the program is frozen'], | ||
| ['Last deployed', 'slot 302582236'], | ||
| ['IDL', 'published on-chain via Anchor, served as Codama'], | ||
| ], | ||
| }, | ||
| { | ||
| kind: 'text', | ||
| text: 'The verified build matches Squads-Protocol/v4 @ 2a47b4c (signer sqdcVVoTcKZjXU8yPUwKFbGx1Hig1rhbWJQtMRXp2E1), and the embedded security.txt names OtterSec and Neodyme as auditors.', | ||
| }, | ||
| ], | ||
| id: 'program', | ||
| prompt: 'Who holds the upgrade authority for this program, and does the verified build match?', | ||
| question: | ||
| 'Who holds the upgrade authority for SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf, and does the verified build match?', | ||
| tool: 'inspect_entity', | ||
| }, | ||
| { | ||
| answer: [ | ||
| { | ||
| kind: 'text', | ||
| text: 'A v0 message, success, finalized at slot 439023338. One signer, fee 5000 lamports, 67,888 CU consumed; 53 accounts, 34 of them pulled in from 5 address lookup tables.', | ||
| }, | ||
| { | ||
| kind: 'text', | ||
| text: 'There are no inner instructions anywhere — three invoke [1] lines in the logs and no [2] depth at all.', | ||
| }, | ||
| { | ||
| head: ['#', 'Program', 'What it did'], | ||
| kind: 'table', | ||
| rows: [ | ||
| ['1', 'ComputeBudget', 'SetComputeUnitLimit 600,000 — no priority fee'], | ||
| ['2', 'System', 'transfer 1002 lamports from the signer'], | ||
| ['3', '8GCr9711…ViXk', '53 accounts, 154 bytes, 67,588 CU — unverified, no IDL, stays raw'], | ||
| ], | ||
| }, | ||
| { | ||
| kind: 'text', | ||
| text: 'A 53-account instruction touching Meteora DLMM and PumpSwap issued zero CPIs and still returned success — the consistent read (inference, not proof) is an arbitrage bot that found nothing worth executing.', | ||
| }, | ||
| ], | ||
| id: 'transaction', | ||
| prompt: 'Walk me through this signature, including the inner instructions.', | ||
| question: | ||
| 'Walk me through 3MVAxtaFp76y23DBd3MdXTEjpzH8zFtVB1HtVdYSKqZPpx1R9gEkDXCF9bX26vkAvyerz2K54eMCFF7cPpkzArM1, including the inner instructions.', | ||
| tool: 'inspect_entity', | ||
| }, | ||
| { | ||
| answer: [ | ||
| { kind: 'text', text: 'USDC (EPjFWdd5…) — SPL Token mint on mainnet-beta.' }, | ||
| { | ||
| head: ['Field', 'Value'], | ||
| kind: 'table', | ||
| rows: [ | ||
| ['Decimals', '6'], | ||
| ['Supply', '7748676460441051 raw → 7,748,676,460.441051 USDC'], | ||
| ['Mint authority', 'BJE5MMbqXjVwjAF7oxwPYXnTXDyspzZyt4vwenNw5ruG'], | ||
| ], | ||
| }, | ||
| { | ||
| kind: 'text', | ||
| text: 'Supply is variable — the mint authority is present. Freeze authority is 7dGbd2QZcCKcTndnHcTL8q7SMVXAkp688NTQYwrRCrar.', | ||
| }, | ||
| ], | ||
| id: 'mint', | ||
| prompt: 'What is the supply and decimals of this mint, and is the supply fixed?', | ||
| question: | ||
| 'What is the supply and decimals of EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, and is the supply fixed?', | ||
| tool: 'inspect_entity', | ||
| }, | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { BookOpen, MessageSquare, Terminal, Tool } from 'react-feather'; | ||
|
|
||
| export const GUTTER = 'px-5 sm:px-8 lg:px-10 xxl:px-14'; | ||
|
|
||
| export const BAND_RULE = 'border-0 border-t border-solid border-dark-border'; | ||
|
|
||
| export const MONO_LABEL = 'font-mono text-xs uppercase tracking-widest'; | ||
|
|
||
| export const SECTIONS = [ | ||
| { Icon: Terminal, id: 'setup', kicker: 'Setup' }, | ||
| { Icon: BookOpen, id: 'instructions', kicker: 'Instructions' }, | ||
| { Icon: Tool, id: 'tools', kicker: 'Tools' }, | ||
| { Icon: MessageSquare, id: 'examples', kicker: 'Examples' }, | ||
| ] as const; | ||
|
|
||
| export enum EndpointState { | ||
| Checking = 'checking', | ||
| Ready = 'ready', | ||
| Restricted = 'restricted', | ||
| Blocked = 'blocked', | ||
| Disabled = 'disabled', | ||
| } | ||
|
|
||
| export type EndpointStatus = { state: EndpointState; ms?: number }; |
Uh oh!
There was an error while loading. Please reload this page.