diff --git a/.github/workflows/deploy-app-walrus.yml b/.github/workflows/deploy-app-walrus.yml index 216545d3..414812aa 100644 --- a/.github/workflows/deploy-app-walrus.yml +++ b/.github/workflows/deploy-app-walrus.yml @@ -99,6 +99,6 @@ jobs: general: wallet_env: mainnet walrus_context: mainnet - walrus_package: 0xfa65cb2d62f4d39e60346fb7d501c12538ca2bbc646eaa37ece2aec5f897814e + walrus_package: 0x98da433aa0139512c210597b1c5e3df6cd121d8d77f8652691bb66fadfc8aa1b gas_budget: 500000000 default_context: mainnet diff --git a/.github/workflows/release-mcp.yml b/.github/workflows/release-mcp.yml new file mode 100644 index 00000000..fa71a75b --- /dev/null +++ b/.github/workflows/release-mcp.yml @@ -0,0 +1,146 @@ +name: Release MCP Package + +on: + push: + branches: + - main + - staging + - dev + paths: + - 'packages/mcp/**' + - '.github/workflows/release-mcp.yml' + workflow_dispatch: + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Version & Publish + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: pnpm + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm --filter @mysten-incubation/memwal-mcp typecheck + + - name: Build MCP package + run: pnpm --filter @mysten-incubation/memwal-mcp build + + # ── main branch → changeset version + stable release (latest) ── + - name: Apply changesets (update version & CHANGELOG) + if: github.ref == 'refs/heads/main' + id: changeset_version + run: | + pnpm changeset version 2>/dev/null || true + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit changelog & version bump + if: github.ref == 'refs/heads/main' && steps.changeset_version.outputs.has_changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore: version packages & update changelog [skip ci]" + git push + + - name: Publish stable release + if: github.ref == 'refs/heads/main' + id: publish_mcp + run: | + VERSION=$(node -p "require('./packages/mcp/package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + npm view @mysten-incubation/memwal-mcp@$VERSION version 2>/dev/null \ + && echo "Version $VERSION already published, skipping" && echo "published=false" >> $GITHUB_OUTPUT && exit 0 + cd packages/mcp && npm publish --provenance --access public + echo "published=true" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + if: github.ref == 'refs/heads/main' && steps.publish_mcp.outputs.published == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const version = '${{ steps.publish_mcp.outputs.version }}'; + const tag = `@mysten-incubation/memwal-mcp@${version}`; + let body = `Release @mysten-incubation/memwal-mcp v${version}`; + try { + const changelog = fs.readFileSync('packages/mcp/CHANGELOG.md', 'utf8'); + const match = changelog.match(/## \d+\.\d+\.\d+[\s\S]*?(?=## \d+\.\d+\.\d+|$)/); + if (match) body = match[0].trim(); + } catch (e) { /* use default body */ } + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + name: tag, + body, + draft: false, + prerelease: false, + }); + + # ── staging branch → release candidate (rc tag, auto-increment) ── + - name: Publish staging release candidate + if: github.ref == 'refs/heads/staging' + run: | + BASE_VERSION=$(node -p "require('./packages/mcp/package.json').version") + LATEST=$(npm view @mysten-incubation/memwal-mcp versions --json 2>/dev/null \ + | node -p " + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const versions = input ? JSON.parse(input) : []; + const nums = (Array.isArray(versions) ? versions : [versions]) + .filter(v => v.startsWith('${BASE_VERSION}-rc.')) + .map(v => +v.split('-rc.')[1]) + .sort((a,b) => b - a); + nums.length ? nums[0] : -1; + " || echo "-1") + NEXT=$((LATEST + 1)) + NEW_VERSION="${BASE_VERSION}-rc.${NEXT}" + echo "Publishing @mysten-incubation/memwal-mcp@${NEW_VERSION}" + cd packages/mcp + node -e "const p=require('./package.json');p.version='${NEW_VERSION}';require('fs').writeFileSync('./package.json',JSON.stringify(p,null,4)+'\n')" + npm publish --tag rc --provenance --access public + + # ── dev branch → prerelease (dev tag, auto-increment) ── + - name: Publish dev prerelease + if: github.ref == 'refs/heads/dev' + run: | + BASE_VERSION=$(node -p "require('./packages/mcp/package.json').version") + LATEST=$(npm view @mysten-incubation/memwal-mcp versions --json 2>/dev/null \ + | node -p " + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const versions = input ? JSON.parse(input) : []; + const nums = (Array.isArray(versions) ? versions : [versions]) + .filter(v => v.startsWith('${BASE_VERSION}-dev.')) + .map(v => +v.split('-dev.')[1]) + .sort((a,b) => b - a); + nums.length ? nums[0] : -1; + " || echo "-1") + NEXT=$((LATEST + 1)) + NEW_VERSION="${BASE_VERSION}-dev.${NEXT}" + echo "Publishing @mysten-incubation/memwal-mcp@${NEW_VERSION}" + cd packages/mcp + node -e "const p=require('./package.json');p.version='${NEW_VERSION}';require('fs').writeFileSync('./package.json',JSON.stringify(p,null,4)+'\n')" + npm publish --tag dev --provenance --access public diff --git a/.gitignore b/.gitignore index 2a06430a..f4fd0694 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,10 @@ tmp-smashblob/ # Railway sensitive docs railway-*.md + +# Per-developer MCP client config (user paths) +.mcp.json + +# Personal/internal planning notes — never include in repo +plans/ +.local-plans/ diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 44bcd944..8934fa6e 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -24,6 +24,7 @@ import LandingPage from './pages/LandingPage' import Dashboard from './pages/Dashboard' import SetupWizard from './pages/SetupWizard' import Playground from './pages/Playground' +import ConnectMcp from './pages/ConnectMcp' import '@mysten/dapp-kit/dist/index.css' @@ -212,6 +213,7 @@ function AppContent() { !currentAccount ? : delegateKey ? : } /> + } /> } /> ) diff --git a/apps/app/src/index.css b/apps/app/src/index.css index ea53adcf..5c6d5227 100644 --- a/apps/app/src/index.css +++ b/apps/app/src/index.css @@ -86,6 +86,7 @@ body, body { background-image: none; + overflow-x: hidden; } h1, h2, h3 { @@ -442,26 +443,37 @@ h1, h2, h3 { } .lp-nav-inner { + position: relative; width: 100%; display: flex; align-items: center; justify-content: space-between; + gap: 20px; + min-width: 0; } .lp-nav-brand { display: flex; align-items: center; text-decoration: none; + min-width: 0; } .lp-nav-brand img { height: 28px; + max-width: min(280px, 46vw); } .lp-nav-links { display: flex; align-items: center; gap: 32px; + min-width: 0; + flex-shrink: 0; +} + +.lp-nav-links > :only-child { + grid-column: 1 / -1; } .lp-nav-links a:not(.lp-nav-cta) { @@ -503,6 +515,13 @@ h1, h2, h3 { cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; box-shadow: 3px 3px 0 #000000; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + line-height: 1.05; + text-align: center; + white-space: nowrap; } .lp-nav-cta:hover { @@ -524,6 +543,7 @@ h1, h2, h3 { .lp-demo-trigger { display: flex; align-items: center; + justify-content: center; gap: 4px; background: none; border: 2px solid #000000; @@ -780,6 +800,21 @@ h1, h2, h3 { width: 100%; } +.lp-login-menu { + width: min(360px, calc(100vw - 48px)); + max-width: calc(100vw - 48px); +} + +.lp-login-provider-btn { + min-width: 0; + overflow: hidden; + white-space: normal; +} + +.lp-login-provider-btn svg { + flex: 0 0 auto; +} + .lp-login-wallet-btn [class*="ConnectButton"], .lp-login-wallet-btn div { width: 100% !important; @@ -814,6 +849,19 @@ h1, h2, h3 { padding: 0 24px; } + .lp-nav-inner { + gap: 16px; + } + + .lp-nav-links { + gap: 14px; + } + + .lp-nav-cta, + .lp-demo-trigger { + padding: 10px 14px; + } + .lp-hero { padding: 0 24px; } @@ -842,7 +890,78 @@ h1, h2, h3 { } } +@media (max-width: 640px) { + .lp-nav { + height: auto; + min-height: 72px; + padding: 12px 18px; + } + + .lp-nav-inner { + align-items: flex-start; + flex-direction: column; + gap: 12px; + } + + .lp-nav-brand img { + max-width: 220px; + } + + .lp-nav-links { + width: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); + gap: 12px; + } + + .lp-demo-dropdown, + .lp-demo-trigger, + .lp-nav-links .lp-nav-cta { + width: 100%; + } + + .lp-demo-menu { + left: 0; + right: auto; + width: min(280px, calc(100vw - 36px)); + } + + .lp-login-dropdown { + position: static; + } + + .lp-login-menu { + left: 0; + right: 0; + width: 100% !important; + max-width: none; + min-width: 0 !important; + } +} + @media (max-width: 480px) { + .lp-nav { + padding-left: 14px; + padding-right: 14px; + } + + .lp-nav-brand img { + max-width: 200px; + } + + .lp-nav-links { + grid-template-columns: 1fr 1fr; + gap: 10px; + } + + .lp-nav-cta, + .lp-demo-trigger { + min-height: 52px; + padding: 9px 10px; + font-size: 0.86rem; + white-space: normal; + } + .lp-hero-copy h1 { font-size: 2rem; } @@ -890,6 +1009,17 @@ h1, h2, h3 { font-size: 0.9rem; } +.dashboard-cta-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.dashboard-cta-row > :only-child { + grid-column: 1 / -1; +} + .dashboard-cta { display: flex; align-items: center; @@ -904,6 +1034,13 @@ h1, h2, h3 { box-shadow: var(--neo-shadow); cursor: pointer; transition: transform 0.1s ease, box-shadow 0.1s ease; + min-width: 0; + margin-bottom: 0; +} + +.dashboard-cta--disabled { + cursor: default; + opacity: 0.75; } .dashboard-cta:hover { @@ -920,12 +1057,14 @@ h1, h2, h3 { font-size: 1.15rem; font-weight: 800; color: #0d0e16; + overflow-wrap: anywhere; } .dashboard-cta-subtitle { font-size: 0.88rem; font-weight: 500; color: #3a3b48; + overflow-wrap: anywhere; } .dashboard-cta-arrow { @@ -1006,6 +1145,14 @@ h1, h2, h3 { display: flex; gap: 8px; margin-top: 12px; + flex-wrap: wrap; +} + +.card-header-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; } .key-display .btn-secondary { @@ -1442,20 +1589,131 @@ h1, h2, h3 { @media (max-width: 640px) { .nav { padding: 0 14px; + height: auto; + min-height: 64px; } .nav-inner { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; height: auto; min-height: 64px; - padding: 12px 0; - flex-wrap: wrap; - gap: 10px; + padding: 14px 0; + gap: 10px 14px; + } + + .nav-brand { + min-width: 0; + } + + .nav-brand img { + max-width: min(220px, 58vw); } .nav-user { + display: contents; + } + + .nav-address { + grid-column: 1 / -1; + grid-row: 2; + justify-self: start; + max-width: 100%; + } + + .nav-user .demo-nav-back { + grid-column: 1 / -1; + grid-row: 2; + justify-self: start; + } + + .nav-user .demo-nav-back + .nav-address { + grid-row: 3; + } + + .nav-user .lp-nav-cta { + grid-column: 2; + grid-row: 1; + min-height: 48px; + padding: 9px 16px; + } + + .dashboard { + padding: 32px 18px; + } + + .dashboard-header { + margin-bottom: 24px; + } + + .dashboard-cta-row { + grid-template-columns: 1fr; + gap: 14px; + } + + .dashboard-cta { + min-height: 116px; + padding: 18px 20px; + } + + .dashboard-cta-title { + font-size: 1.05rem; + } + + .card { + padding: 18px; + } + + .card-header { + align-items: flex-start; + flex-direction: column; + gap: 14px; + } + + .card-header-actions { width: 100%; - justify-content: flex-start; - flex-wrap: wrap; + display: grid; + grid-template-columns: 1fr 1fr; + } + + .card-header-actions .btn, + .card-header-actions .lp-nav-cta { + width: 100%; + min-height: 52px; + } + + .key-display { + padding: 16px; + } + + .key-display .key-actions { + display: grid; + grid-template-columns: 1fr; + } + + .key-display .key-actions .btn { + width: 100%; + } + + .install-tabs { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; + margin-bottom: 8px; + overflow: visible; + } + + .install-tab, + .install-tab:last-child { + border: 2px solid #000000; + border-radius: 10px; + min-height: 54px; + padding: 10px 12px; + } + + .install-tabs + .demo-code-block { + border-top-left-radius: 12px; } .dashboard-cta-arrow { diff --git a/apps/app/src/main.tsx b/apps/app/src/main.tsx index bef5202a..9a754f46 100644 --- a/apps/app/src/main.tsx +++ b/apps/app/src/main.tsx @@ -3,6 +3,20 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +const PRELOAD_RELOAD_KEY = 'memwal:preload-error-reloaded-at' + +window.addEventListener('vite:preloadError', (event) => { + event.preventDefault() + + const now = Date.now() + const lastReloadAt = Number(sessionStorage.getItem(PRELOAD_RELOAD_KEY) || 0) + + if (now - lastReloadAt > 10_000) { + sessionStorage.setItem(PRELOAD_RELOAD_KEY, String(now)) + window.location.reload() + } +}) + createRoot(document.getElementById('root')!).render( diff --git a/apps/app/src/pages/ConnectMcp.tsx b/apps/app/src/pages/ConnectMcp.tsx new file mode 100644 index 00000000..51554fac --- /dev/null +++ b/apps/app/src/pages/ConnectMcp.tsx @@ -0,0 +1,545 @@ +/** + * Connect MCP — browser-based wallet sign-in flow for the `@mysten-incubation/memwal-mcp` + * stdio bridge. + * + * The MCP package opens this page in the user's browser with a query string: + * + * /connect/mcp?port=17463 + * &publicKey=<64-hex Ed25519 pub> + * &delegateAddress=<0x-prefixed Sui address> + * &label= + * &relayer= + * + * Flow: + * 1. Render consent screen — show requested permissions + key fingerprint. + * 2. User clicks "Connect Sui Wallet" → standard dApp Kit wallet popup. + * 3. Build + sign `add_delegate_key(account, publicKey, delegateAddress, label, clock)` + * via useSponsoredTransaction (matches SetupWizard pattern). + * 4. POST result {accountId, walletAddress, packageId, txDigest, label} + * to http://localhost:/callback — the MCP package's listener. + * 5. Show success screen — user can close the tab. + * + * Error paths: + * - Wallet not connected → wallet picker. + * - User has no MemWalAccount yet → link to /setup. + * - Wallet rejects tx → retry button. + * - localhost callback unreachable → keep success on-chain anyway, ask user + * to manually copy creds (rare — only if the MCP listener died). + */ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { + ConnectModal, + useCurrentAccount, + useSuiClient, +} from '@mysten/dapp-kit' +import { Transaction } from '@mysten/sui/transactions' +import { Link, useSearchParams } from 'react-router-dom' +import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' +import { config } from '../config' +import memwalLogo from '../assets/memwal-logo.svg' + +type Step = + | 'consent' + | 'signing' + | 'callback' + | 'success' + | 'no-account' + | 'error' + +function hexToBytes(hex: string): number[] { + const clean = hex.startsWith('0x') ? hex.slice(2) : hex + const out: number[] = [] + for (let i = 0; i < clean.length; i += 2) { + out.push(parseInt(clean.slice(i, i + 2), 16)) + } + return out +} + +async function resolveAccountId( + suiClient: ReturnType, + ownerAddress: string, +): Promise { + try { + const registryObj = await suiClient.getObject({ + id: config.memwalRegistryId, + options: { showContent: true }, + }) + if (registryObj?.data?.content && 'fields' in registryObj.data.content) { + const fields = registryObj.data.content.fields as any + const tableId = fields?.accounts?.fields?.id?.id + if (tableId) { + const dynField = await suiClient.getDynamicFieldObject({ + parentId: tableId, + name: { type: 'address', value: ownerAddress }, + }) + if ( + dynField?.data?.content && + 'fields' in dynField.data.content + ) { + return (dynField.data.content.fields as any).value as string + } + } + } + } catch { + return null + } + return null +} + +interface McpCallbackPayload { + accountId: string + walletAddress: string + packageId: string + txDigest: string + label: string + /** Echoes the state token the bridge issued in the query string. */ + state: string +} + +export default function ConnectMcp() { + const [params] = useSearchParams() + const currentAccount = useCurrentAccount() + const suiClient = useSuiClient() + const { mutateAsync: signAndExecute } = useSponsoredTransaction() + + const port = params.get('port') ?? '' + const publicKey = params.get('publicKey') ?? '' + const delegateAddress = params.get('delegateAddress') ?? '' + const label = params.get('label') ?? 'MemWal MCP' + const relayer = params.get('relayer') ?? 'https://relayer.memwal.ai' + /** + * Cryptographic state token from the MCP bridge. Must be echoed verbatim + * in the callback POST — the bridge constant-time compares it to defeat + * cross-origin CSRF (audit C2). Empty string if absent (older bridge); + * the bridge will then reject our callback with 400. + */ + const state = params.get('state') ?? '' + + const [step, setStep] = useState('consent') + const [errorMsg, setErrorMsg] = useState('') + const [walletPickerOpen, setWalletPickerOpen] = useState(false) + const [callbackPayload, setCallbackPayload] = useState(null) + const [callbackDelivered, setCallbackDelivered] = useState(null) + + // Validate query string up-front. + const paramsValid = useMemo(() => { + const portNum = Number(port) + return ( + Number.isFinite(portNum) && + portNum > 1024 && + portNum < 65536 && + /^[0-9a-fA-F]{64}$/.test(publicKey) && + /^0x[0-9a-fA-F]{64}$/.test(delegateAddress) && + // State token is a 32-byte hex string emitted by the MCP bridge. + // Old bridges without state will fail this check — by design; + // forces a bridge upgrade so we never accept stateless callbacks. + /^[0-9a-f]{64}$/.test(state) + ) + }, [port, publicKey, delegateAddress, state]) + + const postCallback = useCallback( + async (payload: McpCallbackPayload) => { + try { + const res = await fetch(`http://127.0.0.1:${port}/callback`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload), + }) + setCallbackDelivered(res.ok) + } catch { + setCallbackDelivered(false) + } + }, + [port], + ) + + const handleConnect = useCallback(async () => { + if (!paramsValid) { + setErrorMsg('Invalid query parameters from MCP client.') + setStep('error') + return + } + if (!currentAccount) { + setWalletPickerOpen(true) + return + } + + setStep('signing') + try { + // Resolve the user's MemWalAccount. + const accountId = await resolveAccountId(suiClient, currentAccount.address) + if (!accountId) { + setStep('no-account') + return + } + + // Build + sign add_delegate_key tx. + const tx = new Transaction() + tx.moveCall({ + target: `${config.memwalPackageId}::account::add_delegate_key`, + arguments: [ + tx.object(accountId), + tx.pure('vector', hexToBytes(publicKey)), + tx.pure('address', delegateAddress), + tx.pure('string', label), + tx.object('0x6'), + ], + }) + let result + try { + result = await signAndExecute({ transaction: tx }) + } catch (txErr: unknown) { + const m = txErr instanceof Error ? txErr.message : String(txErr) + // Friendly mapping for common contract aborts. + if (m.includes('abort code: 0') && m.includes('add_delegate_key')) { + setErrorMsg( + `This wallet (${currentAccount.address.slice(0, 10)}…${currentAccount.address.slice(-6)}) is not the owner of MemWalAccount ${accountId.slice(0, 10)}…${accountId.slice(-6)}. ` + + `Switch your wallet to the account that originally created this MemWal, OR run /setup to create a new MemWalAccount for the current wallet.` + ) + setStep('error') + return + } + if (m.includes('abort code: 2') && m.includes('add_delegate_key')) { + setErrorMsg( + `This MemWalAccount already has the maximum number of delegate keys (20). Go to /dashboard and revoke an unused key, then try again.` + ) + setStep('error') + return + } + throw txErr + } + await suiClient.waitForTransaction({ digest: result.digest }) + + const payload: McpCallbackPayload = { + accountId, + walletAddress: currentAccount.address, + packageId: config.memwalPackageId, + txDigest: result.digest, + label, + state, + } + setCallbackPayload(payload) + setStep('callback') + await postCallback(payload) + setStep('success') + } catch (err) { + setErrorMsg(err instanceof Error ? err.message : String(err)) + setStep('error') + } + }, [ + paramsValid, + currentAccount, + suiClient, + signAndExecute, + publicKey, + delegateAddress, + label, + state, + postCallback, + ]) + + // If the wallet popup completes after we asked it to open, auto-proceed. + useEffect(() => { + if (!walletPickerOpen && currentAccount && step === 'consent') { + // user picked a wallet — kick off the connect flow. + void handleConnect() + } + // we only want this to fire on wallet→connected transition. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [walletPickerOpen, currentAccount]) + + return ( +
+ + +
+ {!paramsValid && ( +
+

Invalid request

+

+ This page must be opened by the{' '} + @mysten-incubation/memwal-mcp package + during its login flow. +

+

+ Got: port={port || '(none)'}{' '} + publicKey={publicKey ? publicKey.slice(0, 12) + '…' : '(none)'} +

+
+ )} + + {paramsValid && step === 'consent' && ( + + )} + + {paramsValid && step === 'signing' && ( +
+

Confirm in your wallet…

+

+ A wallet popup is registering this delegate key on + chain. Approve the transaction to continue. +

+
+ )} + + {paramsValid && step === 'callback' && ( +
+

Wrapping up…

+

Sending credentials back to your MCP client.

+
+ )} + + {paramsValid && step === 'success' && callbackPayload && ( + + )} + + {paramsValid && step === 'no-account' && ( +
+

Create a MemWal account first

+

+ This wallet doesn't have a MemWalAccount yet. Run + through the one-time setup, then come back here. +

+

+ + Create account + +

+
+ )} + + {paramsValid && step === 'error' && ( +
+

Something went wrong

+

{errorMsg}

+

+ +

+
+ )} +
+ + } + open={walletPickerOpen} + onOpenChange={setWalletPickerOpen} + /> +
+ ) +} + +function ConsentCard({ + label, + delegateAddress, + relayer, + wallet, + onConnect, +}: { + label: string + delegateAddress: string + relayer: string + wallet: string | null + onConnect: () => void +}) { + return ( +
+

+ {label} wants access to + your MemWal memory +

+ +

Permissions requested

+
    +
  • ✓ Read your memories (memwal_recall)
  • +
  • ✓ Save new memories (memwal_remember)
  • +
  • ✓ Extract facts from text (memwal_analyze)
  • +
  • ✓ Re-index from Walrus (memwal_restore)
  • +
+ +

Details

+
+
Relayer
+
+ {relayer} +
+
Delegate address
+
+ {delegateAddress.slice(0, 16)}…{delegateAddress.slice(-6)} +
+
Connected wallet
+
+ {wallet ? ( + + {wallet.slice(0, 12)}…{wallet.slice(-6)} + + ) : ( + (not connected yet) + )} +
+
+ + +
+ ) +} + +function SuccessCard({ + payload, + callbackDelivered, + port, +}: { + payload: McpCallbackPayload + callbackDelivered: boolean | null + port: string +}) { + return ( +
+

+ MCP client connected +

+ {callbackDelivered === true && ( +

+ Credentials were handed off to your MCP client. You can close + this tab safely. +

+ )} + {callbackDelivered === false && ( +

+ The on-chain registration succeeded, but the local MCP login + listener at http://127.0.0.1:{port}/callback{' '} + did not accept the callback. Restart the MCP login command and + try again so credentials can be saved locally. +

+ )} +
+
Account
+
+ {payload.accountId} +
+
+

+ + Go to dashboard + +

+
+ ) +} + +// ---------- styles (match Dashboard's neo-brutalism .card pattern) ---------- + +const mcpNavBrandStyle: React.CSSProperties = { + gap: 12, +} + +const mcpNavTitleStyle: React.CSSProperties = { + fontSize: '1rem', + fontWeight: 700, + color: '#000', + lineHeight: 1, + transform: 'translateY(8px)', +} + +const pageStyle: React.CSSProperties = { + minHeight: '100vh', + background: '#FAF8F5', // same as --color-tusk used by .card / body + color: '#1a1a1a', +} + +const mainStyle: React.CSSProperties = { + maxWidth: 640, + margin: '40px auto', + padding: '0 24px', + display: 'flex', + flexDirection: 'column', + gap: 24, +} + +const cardStyle: React.CSSProperties = { + background: '#fff', + border: '2px solid #000', + borderRadius: 12, + padding: 28, + boxShadow: '4px 4px 0 #000', +} + +const h1Style: React.CSSProperties = { + fontSize: 22, + fontWeight: 800, + margin: '0 0 12px', + letterSpacing: -0.3, +} + +const h3Style: React.CSSProperties = { + fontSize: 12, + fontWeight: 700, + textTransform: 'uppercase' as const, + letterSpacing: 0.6, + color: '#525252', + marginTop: 20, + marginBottom: 8, +} + +const ulStyle: React.CSSProperties = { + listStyle: 'none', + padding: 0, + margin: '0 0 8px', + lineHeight: 1.9, + fontSize: 14, +} + +const dlStyle: React.CSSProperties = { + fontSize: 13, + lineHeight: 1.6, + margin: '0 0 20px', + wordBreak: 'break-all' as const, +} + +const subtleStyle: React.CSSProperties = { + color: '#525252', + fontSize: 14, +} + +const primaryButton: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + padding: '12px 26px', + background: '#E8FF75', + color: '#000', + borderRadius: 12, + border: '2px solid #000', + fontSize: '0.94rem', + fontWeight: 700, + cursor: 'pointer', + textDecoration: 'none', + boxShadow: '3px 3px 0 #000', +} diff --git a/apps/app/src/pages/Dashboard.tsx b/apps/app/src/pages/Dashboard.tsx index 268b7fdb..68607274 100644 --- a/apps/app/src/pages/Dashboard.tsx +++ b/apps/app/src/pages/Dashboard.tsx @@ -391,9 +391,9 @@ const result = await generateText({ )} {/* Action CTAs */} -
+
{delegateKey ? ( - +
try interactive demo @@ -405,7 +405,7 @@ const result = await generateText({
) : hasMaxDelegateKeys ? ( -
+
remove a key first @@ -417,7 +417,7 @@ const result = await generateText({
) : ( - +
create delegate key @@ -430,7 +430,7 @@ const result = await generateText({ )} {config.docsUrl && ( - +
documentation @@ -532,7 +532,7 @@ const result = await generateText({ all Ed25519 keys registered on your MemWalAccount
-
+
) : ( -
+
{loginOpen && ( -
+
{hasEnokiConfig && googleWallet && (