Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .agent/context/20260913T071500Z-mcp-profile-docs-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Session Context: MCP profile docs and bearer copy

## Date/time

- UTC: 2026-09-13T07:15:00Z

## User goal

Make MCP onboarding easier from Profile by linking directly to `/docs/mcp` and
letting the user copy the newly issued personal bearer token.

## Original prompt/request

The user asked to add the documentation link
`https://oneshot.kapustazh.dev/docs/mcp` to Profile and add a button that copies
the bearer token.

## Assumptions

- Copying is available only while the plaintext token is already displayed
after generation or rotation.
- The token remains memory-only and is never persisted by the frontend.
- This branch stacks on the deployed Privy Buffer compatibility fix so a new
frontend deployment does not regress that fix.

## Plan

1. Add the internal documentation link to the MCP profile panel.
2. Reuse the browser Clipboard API to copy only the displayed bearer.
3. Add focused UI coverage and validate the web package.

## Key decisions

- Use `navigator.clipboard` directly; no dependency or storage is needed.
- Report copy failure in the existing profile error surface.

## Files/components touched

- `apps/web/src/components/McpProfile.tsx`: documentation link and copy action.
- `apps/web/test/app-composition.test.tsx`: link and clipboard behavior coverage.

## Commands/checks

- `pnpm --filter @oneshot/web test -- app-composition.test.tsx` - PASS,
7 tests.
- `pnpm --filter @oneshot/web typecheck` - PASS.
- `pnpm --filter @oneshot/web test` - PASS, 17 files / 93 tests.
- `pnpm --filter @oneshot/web test:browser` - PASS, 8 Chromium tests.
- `pnpm lint` - PASS.
- `pnpm format:check` - PASS.
- `git diff --check` - PASS.
- Local Node is v22.23.2 while the repository requests v24.19.0.

## External-doc findings

- None.

## Unresolved questions

- None.

## Git and PR state

- Branch: `feat/mcp-profile-docs-copy`
- Base: `fix/privy-browser-buffer` at
`dcc730792440e3f90cb5e96fe0597f80072a51d0`
- Implementation commit: `8dbbf16f6c3f3682693d7d5bde55dcfaf2b984cb`
- PR: [#127](https://github.com/SWOFART/OneShot/pull/127), draft, stacked
on `fix/privy-browser-buffer`
- CI: repository policy, Markdown/Mermaid, frontend browser acceptance, and
Cloudflare Workers build passed; ESLint/TypeScript was pending at this snapshot.
- Production frontend: Cloudflare Worker version
`11981c12-6a81-4e4a-9ec9-819d6b52d458`; the public entry bundle contains the
documentation link, bearer-copy action, and the preceding Buffer polyfill.

## Review gates

- Gate A: NOT RUN under the user's standing explicit instruction to continue
without FreePi; no PASS is claimed.
- Gate B: NOT RUN

## Handoff/next steps

1. Wait for the final PR check and leave both stacked PRs for human review;
agents do not merge.
19 changes: 19 additions & 0 deletions apps/web/src/components/McpProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function configFor(token: string): string {
export function McpProfile(props: { readonly client: JobApiClient }) {
const [status, setStatus] = useState<McpCredentialStatus | null>(null);
const [issued, setIssued] = useState<IssuedMcpCredential | null>(null);
const [copied, setCopied] = useState(false);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);

Expand All @@ -36,6 +37,7 @@ export function McpProfile(props: { readonly client: JobApiClient }) {

async function issue(): Promise<void> {
setBusy(true);
setCopied(false);
setError(null);
try {
const credential = await props.client.issueMcpCredential(status?.configured === true);
Expand All @@ -52,11 +54,25 @@ export function McpProfile(props: { readonly client: JobApiClient }) {
}
}

async function copyBearer(): Promise<void> {
try {
if (!issued || !navigator.clipboard) throw new Error('Clipboard is unavailable');
await navigator.clipboard.writeText(issued.bearer_token);
setCopied(true);
} catch {
setCopied(false);
setError('Could not copy the bearer token.');
}
}

return (
<section className="panel" aria-labelledby="profile-heading">
<p className="eyebrow">PROFILE / AGENT ACCESS</p>
<h2 id="profile-heading">Connect your agent</h2>
<p>Your bearer is bound to this Privy account and its private request workspace.</p>
<p>
<a href="/docs/mcp">Open MCP documentation</a>
</p>

<button type="button" disabled={busy} onClick={() => void issue()}>
{busy
Expand All @@ -79,6 +95,9 @@ export function McpProfile(props: { readonly client: JobApiClient }) {
<pre className="docs-code" aria-label="Personal MCP client configuration">
<code>{configFor(issued.bearer_token)}</code>
</pre>
<button type="button" className="btn-copy" onClick={() => void copyBearer()}>
{copied ? 'Bearer copied' : 'Copy bearer token'}
</button>
<p>
Request key: <code>{issued.request_key}</code>
</p>
Expand Down
14 changes: 12 additions & 2 deletions apps/web/test/app-composition.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import {
} from '@oneshot/settlement-ui';
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { App } from '../src/App.js';
import { OneShotApiClient } from '../src/api/client.js';
import type { JobApiClient } from '../src/api/job-client.js';
import { signedInSession } from './support/fake-session.js';

afterEach(cleanup);
afterEach(() => {
vi.restoreAllMocks();
cleanup();
});

describe('Gate P5 shell composition', () => {
it('separates the public landing page from the authenticated cabinet route', () => {
Expand Down Expand Up @@ -106,6 +109,7 @@ describe('Gate P5 shell composition', () => {

it('generates a personal MCP bearer in Profile', async () => {
const user = userEvent.setup();
const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue();
const jobClient = {
async mcpCredentialStatus() {
return { configured: false, request_key: 'profile-request' };
Expand All @@ -129,10 +133,16 @@ describe('Gate P5 shell composition', () => {
);

await user.click(screen.getByRole('tab', { name: 'Profile' }));
expect(screen.getByRole('link', { name: 'Open MCP documentation' }).getAttribute('href')).toBe(
'/docs/mcp',
);
await user.click(await screen.findByRole('button', { name: 'Generate bearer token' }));
expect(
(await screen.findByLabelText('Personal MCP client configuration')).textContent,
).toContain('Bearer personal-secret-token');
await user.click(screen.getByRole('button', { name: 'Copy bearer token' }));
expect(writeText).toHaveBeenCalledWith('personal-secret-token');
expect(screen.getByRole('button', { name: 'Bearer copied' })).toBeTruthy();
expect(screen.getByText('profile-request')).toBeTruthy();
});

Expand Down
Loading