mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Governed MCP access spans contracts, runtime enforcement, adapters, UI surfaces, and operator verification > - The parity reference PR #9534 is too large for effective automated or human review > - The feature therefore needs a linear stack whose individual diffs stay below the 100-file review limit > - This pull request is split 1/8 and focuses on fixture and demo MCP servers > - The benefit is a standalone, testable review boundary while preserving byte-for-byte parity at the top of the stack ## Linked Issues or Issue Description - Related parity reference: #9534 - Problem: Developers need deterministic local MCP fixtures and visible demo servers without pulling in the governed production runtime. - Proposed solution: Adds the Google Sheets and KV demo MCP packages, fixture catalog/servers, smoke harness, guide, and the root smoke/typecheck registration hunks. - Alternatives considered: keeping #9534 as one 403-file review, or rewriting the feature to manufacture seams; both were rejected in favor of path extraction plus compile-driven boundary moves. - Roadmap alignment: this advances the existing governed MCP/tool-access work already represented by #9534; it does not introduce a separate roadmap initiative. - Stack position: base branch is `master`. - Merge policy: merge bottom-up, in order, only after the complete eight-PR stack has been reviewed and the top-of-stack parity gate remains empty. - Requested review: QA for fixture and smoke coverage; Greptile on every PR. ## What Changed - Adds the Google Sheets and KV demo MCP packages, fixture catalog/servers, smoke harness, guide, and the root smoke/typecheck registration hunks. - Keeps this PR below 100 changed files and independently typecheckable. - Preserves the final tree from #9534 when combined with the other seven stack levels. ## Verification - `pnpm typecheck` - `pnpm --filter @paperclipai/google-sheets-mcp-server test` — 27 tests passed - `pnpm --filter @paperclipai/kv-demo-mcp-server test` — 12 tests passed ## Risks - The new packages add dependencies that are intentionally not committed to `pnpm-lock.yaml`, per repository policy. - Stack risk: merging out of order can expose incomplete layers; mitigate by following the documented bottom-up merge policy. - Parity risk: later edits to an intermediate branch can drift from #9534; mitigate by re-running the empty top-of-stack diff before merge. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, exact model ID `gpt-5.4`; runtime-managed context window; medium reasoning with repository, shell, Git, GitHub CLI, and code-execution tools enabled. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] Internal references are omitted except the execution-plan link explicitly required for this coordinated split stack - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge ## Stack Coordination - Internal execution plan: [PAP-13874](/PAP/issues/PAP-13874#document-plan) - Parity reference: #9534 - Stack: #9556 → #9557 → #9558 → #9559 → #9560 → #9561 → #9562 → #9563 - Merge bottom-up only after full-stack review and an empty parity diff at #9563. --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
import type { KvStateSnapshot } from "./store.js";
|
|
|
|
export interface RenderOptions {
|
|
tokenRequired?: boolean;
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function renderRows(snapshot: KvStateSnapshot): string {
|
|
if (snapshot.entries.length === 0) {
|
|
return `<tr class="empty"><td colspan="3">No values yet — call the <code>kv_set</code> tool to add one.</td></tr>`;
|
|
}
|
|
return snapshot.entries
|
|
.map(
|
|
(entry) => `<tr>
|
|
<td class="key">${escapeHtml(entry.key)}</td>
|
|
<td class="value">${escapeHtml(entry.value)}</td>
|
|
<td class="updated">${escapeHtml(entry.updatedAt)}</td>
|
|
</tr>`,
|
|
)
|
|
.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Render the values UI. The page server-renders the current snapshot and then
|
|
* polls {@code GET /api/state} so the table reflects tool writes without a manual
|
|
* refresh.
|
|
*/
|
|
export function renderStatePage(snapshot: KvStateSnapshot, options: RenderOptions = {}): string {
|
|
const tokenRequired = options.tokenRequired === true;
|
|
|
|
return `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>KV Demo MCP Server</title>
|
|
<style>
|
|
:root { color-scheme: light dark; }
|
|
body { font: 14px/1.5 system-ui, sans-serif; margin: 0; padding: 2rem; }
|
|
h1 { font-size: 1.25rem; margin: 0 0 0.25rem; }
|
|
.meta { color: #6b7280; margin: 0 0 1.5rem; }
|
|
.meta strong { color: inherit; }
|
|
table { border-collapse: collapse; width: 100%; max-width: 960px; }
|
|
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
|
th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280; }
|
|
td.key { font-weight: 600; }
|
|
td.value { white-space: pre-wrap; word-break: break-word; }
|
|
td.updated { color: #6b7280; white-space: nowrap; }
|
|
tr.empty td { color: #6b7280; font-style: italic; }
|
|
code { background: rgba(127,127,127,0.18); padding: 0.05rem 0.3rem; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>KV Demo MCP Server</h1>
|
|
<p class="meta">In-memory values for this process. <strong id="count">${snapshot.count}</strong> key(s), revision <strong id="revision">${snapshot.revision}</strong>. <span id="status">Auto-refreshing every 2s.</span></p>
|
|
<table>
|
|
<thead><tr><th>Key</th><th>Value</th><th>Updated</th></tr></thead>
|
|
<tbody id="rows">
|
|
${renderRows(snapshot)}
|
|
</tbody>
|
|
</table>
|
|
<script>
|
|
const TOKEN_REQUIRED = ${JSON.stringify(tokenRequired)};
|
|
const fragment = new URLSearchParams(window.location.hash.slice(1));
|
|
const token = fragment.get("token") || "";
|
|
if (window.location.hash) history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
function escapeHtml(value) {
|
|
return String(value).replace(/[&<>"']/g, (ch) => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
})[ch]);
|
|
}
|
|
function render(state) {
|
|
document.getElementById("count").textContent = state.count;
|
|
document.getElementById("revision").textContent = state.revision;
|
|
const rows = document.getElementById("rows");
|
|
if (!state.entries.length) {
|
|
rows.innerHTML = '<tr class="empty"><td colspan="3">No values yet — call the <code>kv_set</code> tool to add one.</td></tr>';
|
|
return;
|
|
}
|
|
rows.innerHTML = state.entries.map((entry) =>
|
|
'<tr><td class="key">' + escapeHtml(entry.key) +
|
|
'</td><td class="value">' + escapeHtml(entry.value) +
|
|
'</td><td class="updated">' + escapeHtml(entry.updatedAt) + '</td></tr>'
|
|
).join("");
|
|
}
|
|
async function refresh() {
|
|
const status = document.getElementById("status");
|
|
try {
|
|
const headers = { accept: "application/json" };
|
|
if (token) headers.authorization = "Bearer " + token;
|
|
if (TOKEN_REQUIRED && !token) throw new Error("Add #token=YOUR_TOKEN to this URL");
|
|
const res = await fetch("/api/state", { headers });
|
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
render(await res.json());
|
|
status.textContent = "Auto-refreshing every 2s.";
|
|
} catch (err) {
|
|
status.textContent = "Refresh failed: " + err.message;
|
|
}
|
|
}
|
|
setInterval(refresh, 2000);
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|