Skip to content

Commit 04e2e7d

Browse files
authored
Merge pull request #75 from openagentlock/codex/oal75-mcp-pending-main
Add MCP pending pin workflow
2 parents 92599df + fab6e1f commit 04e2e7d

6 files changed

Lines changed: 469 additions & 7 deletions

File tree

control-plane/api/openapi.yaml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ paths:
135135
"200":
136136
description: pinned
137137

138+
/v1/mcp/pin/refuse:
139+
post:
140+
summary: Refuse a pending MCP fingerprint.
141+
requestBody:
142+
required: true
143+
content:
144+
application/json:
145+
schema: { $ref: '#/components/schemas/McpPinAccept' }
146+
responses:
147+
"200":
148+
description: refused
149+
138150
/v1/ledger/tail:
139151
get:
140152
summary: Server-sent events stream of ledger appends.
@@ -344,11 +356,27 @@ components:
344356

345357
McpPinsResponse:
346358
type: object
347-
required: [pins]
359+
required: [pins, pending]
348360
properties:
349361
pins:
350362
type: array
351363
items: { $ref: '#/components/schemas/McpPin' }
364+
pending:
365+
type: array
366+
items: { $ref: '#/components/schemas/PendingMcpPin' }
367+
368+
PendingMcpPin:
369+
type: object
370+
required: [id, server, fingerprint, status, created_at, updated_at]
371+
properties:
372+
id: { type: string }
373+
server: { type: string }
374+
fingerprint: { type: string }
375+
known_fingerprint: { type: string }
376+
status: { type: string, enum: [unknown, changed] }
377+
server_info: { type: object, additionalProperties: true }
378+
created_at: { type: string, format: date-time }
379+
updated_at: { type: string, format: date-time }
352380

353381
LedgerRoot:
354382
type: object

control-plane/dashboard-ui/src/lib/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,20 @@ export interface MCPPinRow {
6363
fingerprint: string;
6464
}
6565

66+
export interface PendingMCPPinRow {
67+
id: string;
68+
server: string;
69+
fingerprint: string;
70+
known_fingerprint?: string;
71+
status: "unknown" | "changed";
72+
server_info?: Record<string, unknown>;
73+
created_at: string;
74+
updated_at: string;
75+
}
76+
6677
export interface MCPPinsResponse {
6778
pins: MCPPinRow[];
79+
pending: PendingMCPPinRow[];
6880
}
6981

7082
export interface RootInfo {

control-plane/dashboard-ui/src/routes/mcp.tsx

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,157 @@
1+
import { useState } from "react";
12
import { createFileRoute } from "@tanstack/react-router";
23
import { useMCPPins } from "@/hooks/usePoll";
4+
import { apiSend } from "@/lib/api";
35
import { shortHash } from "@/lib/filters";
6+
import { fullLocal, shortTime } from "@/lib/time";
7+
import type { PendingMCPPinRow } from "@/lib/types";
48

59
function MCPTab() {
6-
const { data, error } = useMCPPins(true);
10+
const { data, error, refresh } = useMCPPins(true);
711
const pins = data?.pins ?? [];
12+
const pending = data?.pending ?? [];
13+
const [acting, setActing] = useState<string | null>(null);
14+
const [actionError, setActionError] = useState<string | null>(null);
15+
const [selected, setSelected] = useState<PendingMCPPinRow | null>(null);
16+
17+
const act = async (row: PendingMCPPinRow, action: "accept" | "refuse") => {
18+
setActing(`${action}:${row.id}`);
19+
setActionError(null);
20+
try {
21+
await apiSend(`/v1/mcp/pin/${action}`, "POST", {
22+
server: row.server,
23+
fingerprint: row.fingerprint,
24+
});
25+
await refresh();
26+
setSelected(null);
27+
} catch (e) {
28+
setActionError(e instanceof Error ? e.message : String(e));
29+
} finally {
30+
setActing(null);
31+
}
32+
};
833

934
return (
1035
<div className="space-y-4">
36+
<section className="oal-panel">
37+
<div className="flex items-center gap-3 mb-3">
38+
<div className="text-[11px] uppercase tracking-wider text-muted">Pending MCP pins</div>
39+
<div className="text-[11px] text-muted font-mono ml-2">{pending.length} pending</div>
40+
</div>
41+
42+
{(error || actionError) && (
43+
<div className="text-xs text-deny mb-2">{actionError ?? error}</div>
44+
)}
45+
46+
<div className="overflow-x-auto">
47+
<table className="oal-table">
48+
<thead>
49+
<tr>
50+
<th>server</th>
51+
<th>status</th>
52+
<th>fingerprint</th>
53+
<th>seen</th>
54+
<th>actions</th>
55+
</tr>
56+
</thead>
57+
<tbody>
58+
{pending.length === 0 ? (
59+
<tr>
60+
<td colSpan={5} className="text-center text-muted py-4">
61+
no pending MCP pins
62+
</td>
63+
</tr>
64+
) : (
65+
pending.map((row) => (
66+
<tr key={row.id}>
67+
<td className="font-mono">{row.server}</td>
68+
<td>
69+
<span className="oal-chip">{row.status}</span>
70+
</td>
71+
<td className="font-mono text-muted" title={row.fingerprint}>
72+
{shortHash(row.fingerprint, 24)}
73+
{row.known_fingerprint && (
74+
<span className="block text-[10px]" title={row.known_fingerprint}>
75+
was {shortHash(row.known_fingerprint, 18)}
76+
</span>
77+
)}
78+
</td>
79+
<td className="font-mono" title={fullLocal(row.updated_at)}>
80+
{shortTime(row.updated_at)}
81+
</td>
82+
<td>
83+
<div className="flex gap-2">
84+
<button
85+
className="oal-btn text-xs"
86+
disabled={acting !== null}
87+
onClick={() => void act(row, "accept")}
88+
>
89+
{acting === `accept:${row.id}` ? "Accepting" : "Accept"}
90+
</button>
91+
<button className="oal-btn text-xs" onClick={() => setSelected(row)}>
92+
Details
93+
</button>
94+
<button
95+
className="oal-btn text-xs"
96+
disabled={acting !== null}
97+
onClick={() => void act(row, "refuse")}
98+
>
99+
{acting === `refuse:${row.id}` ? "Refusing" : "Refuse"}
100+
</button>
101+
</div>
102+
</td>
103+
</tr>
104+
))
105+
)}
106+
</tbody>
107+
</table>
108+
</div>
109+
</section>
110+
111+
{selected && (
112+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
113+
<div className="w-full max-w-xl rounded-md border border-border bg-panel p-4 shadow-xl">
114+
<div className="flex items-center justify-between gap-3 border-b border-border pb-3">
115+
<div>
116+
<div className="text-[11px] uppercase tracking-wider text-muted">
117+
MCP pin request
118+
</div>
119+
<div className="font-mono text-sm">{selected.server}</div>
120+
</div>
121+
<button className="oal-btn-link" onClick={() => setSelected(null)}>
122+
Close
123+
</button>
124+
</div>
125+
<div className="mt-3 space-y-3 text-xs">
126+
<div>
127+
<div className="text-muted">fingerprint</div>
128+
<div className="font-mono break-all">{selected.fingerprint}</div>
129+
</div>
130+
{selected.known_fingerprint && (
131+
<div>
132+
<div className="text-muted">known fingerprint</div>
133+
<div className="font-mono break-all">{selected.known_fingerprint}</div>
134+
</div>
135+
)}
136+
<div>
137+
<div className="text-muted">server info</div>
138+
<pre className="mt-1 max-h-56 overflow-auto rounded border border-border bg-bg p-2 font-mono text-[11px]">
139+
{JSON.stringify(selected.server_info ?? {}, null, 2)}
140+
</pre>
141+
</div>
142+
<div className="flex justify-end gap-2 pt-2">
143+
<button className="oal-btn" onClick={() => void act(selected, "accept")}>
144+
Accept
145+
</button>
146+
<button className="oal-btn" onClick={() => void act(selected, "refuse")}>
147+
Refuse
148+
</button>
149+
</div>
150+
</div>
151+
</div>
152+
</div>
153+
)}
154+
11155
<section className="oal-panel">
12156
<div className="flex items-center gap-3 mb-3">
13157
<div className="text-[11px] uppercase tracking-wider text-muted">MCP pins</div>

0 commit comments

Comments
 (0)