Skip to content

Commit 649ba9b

Browse files
authored
Merge pull request #67 from openagentlock/codex/oal43-mcp-pins-list
Add MCP pins list
2 parents 35ee1df + 340969d commit 649ba9b

9 files changed

Lines changed: 249 additions & 12 deletions

File tree

control-plane/api/openapi.yaml

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,17 @@ paths:
111111
description: verdict
112112
content:
113113
application/json:
114-
schema: { $ref: '#/components/schemas/GateCheckResponse' }
114+
schema: { $ref: '#/components/schemas/McpPinCheckResponse' }
115+
116+
/v1/mcp/pins:
117+
get:
118+
summary: List pinned MCP server fingerprints.
119+
responses:
120+
"200":
121+
description: current pins
122+
content:
123+
application/json:
124+
schema: { $ref: '#/components/schemas/McpPinsResponse' }
115125

116126
/v1/mcp/pin/accept:
117127
post:
@@ -121,7 +131,9 @@ paths:
121131
content:
122132
application/json:
123133
schema: { $ref: '#/components/schemas/McpPinAccept' }
124-
responses: { "204": { description: pinned } }
134+
responses:
135+
"200":
136+
description: pinned
125137

126138
/v1/ledger/tail:
127139
get:
@@ -300,20 +312,44 @@ components:
300312

301313
McpPinCheck:
302314
type: object
303-
required: [server_label, fingerprint]
315+
required: [server, fingerprint]
304316
properties:
305-
server_label: { type: string }
317+
server: { type: string }
306318
fingerprint: { type: string }
307319

320+
McpPinCheckResponse:
321+
type: object
322+
required: [status, server, fingerprint]
323+
properties:
324+
status: { type: string, enum: [unknown, known, changed] }
325+
server: { type: string }
326+
fingerprint: { type: string }
327+
known_fingerprint: { type: string }
328+
308329
McpPinAccept:
309330
type: object
310-
required: [server_label, fingerprint, server_info, signature]
331+
required: [server, fingerprint]
311332
properties:
312-
server_label: { type: string }
333+
server: { type: string }
313334
fingerprint: { type: string }
314335
server_info: { type: object, additionalProperties: true }
315336
signature: { type: string }
316337

338+
McpPin:
339+
type: object
340+
required: [server, fingerprint]
341+
properties:
342+
server: { type: string }
343+
fingerprint: { type: string }
344+
345+
McpPinsResponse:
346+
type: object
347+
required: [pins]
348+
properties:
349+
pins:
350+
type: array
351+
items: { $ref: '#/components/schemas/McpPin' }
352+
317353
LedgerRoot:
318354
type: object
319355
required: [root, seq, genesis_pubkey, computed_at]

control-plane/dashboard-ui/src/hooks/usePoll.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22
import { apiJSON, apiSend } from "@/lib/api";
3-
import type { ModeInfo, PolicyView, RootInfo, SessionsResponse } from "@/lib/types";
3+
import type { MCPPinsResponse, ModeInfo, PolicyView, RootInfo, SessionsResponse } from "@/lib/types";
44

55
export function usePoll(fn: () => void | Promise<void>, intervalMs: number, paused = false): void {
66
const fnRef = useRef(fn);
@@ -136,3 +136,26 @@ export function useSessions(active: boolean): {
136136

137137
return { data, error, refresh };
138138
}
139+
140+
export function useMCPPins(active: boolean): {
141+
data: MCPPinsResponse | null;
142+
error: string | null;
143+
refresh: () => Promise<void>;
144+
} {
145+
const [data, setData] = useState<MCPPinsResponse | null>(null);
146+
const [error, setError] = useState<string | null>(null);
147+
148+
const refresh = useCallback(async () => {
149+
try {
150+
const pins = await apiJSON<MCPPinsResponse>("/v1/mcp/pins");
151+
setData(pins);
152+
setError(null);
153+
} catch (e) {
154+
setError(e instanceof Error ? e.message : String(e));
155+
}
156+
}, []);
157+
158+
usePoll(refresh, 5000, !active);
159+
160+
return { data, error, refresh };
161+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export interface SessionsResponse {
5858
sessions: SessionRow[];
5959
}
6060

61+
export interface MCPPinRow {
62+
server: string;
63+
fingerprint: string;
64+
}
65+
66+
export interface MCPPinsResponse {
67+
pins: MCPPinRow[];
68+
}
69+
6170
export interface RootInfo {
6271
root: string;
6372
seq: number;

control-plane/dashboard-ui/src/routeTree.gen.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as SessionsRouteImport } from './routes/sessions'
1313
import { Route as RulesRouteImport } from './routes/rules'
14+
import { Route as McpRouteImport } from './routes/mcp'
1415
import { Route as EventsRouteImport } from './routes/events'
1516
import { Route as IndexRouteImport } from './routes/index'
1617

@@ -24,6 +25,11 @@ const RulesRoute = RulesRouteImport.update({
2425
path: '/rules',
2526
getParentRoute: () => rootRouteImport,
2627
} as any)
28+
const McpRoute = McpRouteImport.update({
29+
id: '/mcp',
30+
path: '/mcp',
31+
getParentRoute: () => rootRouteImport,
32+
} as any)
2733
const EventsRoute = EventsRouteImport.update({
2834
id: '/events',
2935
path: '/events',
@@ -38,33 +44,37 @@ const IndexRoute = IndexRouteImport.update({
3844
export interface FileRoutesByFullPath {
3945
'/': typeof IndexRoute
4046
'/events': typeof EventsRoute
47+
'/mcp': typeof McpRoute
4148
'/rules': typeof RulesRoute
4249
'/sessions': typeof SessionsRoute
4350
}
4451
export interface FileRoutesByTo {
4552
'/': typeof IndexRoute
4653
'/events': typeof EventsRoute
54+
'/mcp': typeof McpRoute
4755
'/rules': typeof RulesRoute
4856
'/sessions': typeof SessionsRoute
4957
}
5058
export interface FileRoutesById {
5159
__root__: typeof rootRouteImport
5260
'/': typeof IndexRoute
5361
'/events': typeof EventsRoute
62+
'/mcp': typeof McpRoute
5463
'/rules': typeof RulesRoute
5564
'/sessions': typeof SessionsRoute
5665
}
5766
export interface FileRouteTypes {
5867
fileRoutesByFullPath: FileRoutesByFullPath
59-
fullPaths: '/' | '/events' | '/rules' | '/sessions'
68+
fullPaths: '/' | '/events' | '/mcp' | '/rules' | '/sessions'
6069
fileRoutesByTo: FileRoutesByTo
61-
to: '/' | '/events' | '/rules' | '/sessions'
62-
id: '__root__' | '/' | '/events' | '/rules' | '/sessions'
70+
to: '/' | '/events' | '/mcp' | '/rules' | '/sessions'
71+
id: '__root__' | '/' | '/events' | '/mcp' | '/rules' | '/sessions'
6372
fileRoutesById: FileRoutesById
6473
}
6574
export interface RootRouteChildren {
6675
IndexRoute: typeof IndexRoute
6776
EventsRoute: typeof EventsRoute
77+
McpRoute: typeof McpRoute
6878
RulesRoute: typeof RulesRoute
6979
SessionsRoute: typeof SessionsRoute
7080
}
@@ -85,6 +95,13 @@ declare module '@tanstack/react-router' {
8595
preLoaderRoute: typeof RulesRouteImport
8696
parentRoute: typeof rootRouteImport
8797
}
98+
'/mcp': {
99+
id: '/mcp'
100+
path: '/mcp'
101+
fullPath: '/mcp'
102+
preLoaderRoute: typeof McpRouteImport
103+
parentRoute: typeof rootRouteImport
104+
}
88105
'/events': {
89106
id: '/events'
90107
path: '/events'
@@ -105,6 +122,7 @@ declare module '@tanstack/react-router' {
105122
const rootRouteChildren: RootRouteChildren = {
106123
IndexRoute: IndexRoute,
107124
EventsRoute: EventsRoute,
125+
McpRoute: McpRoute,
108126
RulesRoute: RulesRoute,
109127
SessionsRoute: SessionsRoute,
110128
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function RootLayout() {
1414
{ to: "/events", label: "Events" },
1515
{ to: "/rules", label: "Rules" },
1616
{ to: "/sessions", label: "Sessions" },
17+
{ to: "/mcp", label: "MCP" },
1718
]}
1819
/>
1920
<main className="p-5">
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
import { useMCPPins } from "@/hooks/usePoll";
3+
import { shortHash } from "@/lib/filters";
4+
5+
function MCPTab() {
6+
const { data, error } = useMCPPins(true);
7+
const pins = data?.pins ?? [];
8+
9+
return (
10+
<div className="space-y-4">
11+
<section className="oal-panel">
12+
<div className="flex items-center gap-3 mb-3">
13+
<div className="text-[11px] uppercase tracking-wider text-muted">MCP pins</div>
14+
<div className="text-[11px] text-muted font-mono ml-2">{pins.length} pinned</div>
15+
</div>
16+
17+
{error && <div className="text-xs text-deny mb-2">{error}</div>}
18+
19+
<div className="overflow-x-auto">
20+
<table className="oal-table">
21+
<thead>
22+
<tr>
23+
<th>server</th>
24+
<th>fingerprint</th>
25+
</tr>
26+
</thead>
27+
<tbody>
28+
{pins.length === 0 ? (
29+
<tr>
30+
<td colSpan={2} className="text-center text-muted py-4">
31+
no MCP pins
32+
</td>
33+
</tr>
34+
) : (
35+
pins.map((pin) => (
36+
<tr key={pin.server}>
37+
<td className="font-mono">{pin.server}</td>
38+
<td className="font-mono text-muted" title={pin.fingerprint}>
39+
{shortHash(pin.fingerprint, 24)}
40+
</td>
41+
</tr>
42+
))
43+
)}
44+
</tbody>
45+
</table>
46+
</div>
47+
</section>
48+
</div>
49+
);
50+
}
51+
52+
export const Route = createFileRoute("/mcp")({
53+
component: MCPTab,
54+
});

control-plane/internal/api/mcp.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ type pinCheckResponse struct {
3838
KnownFingerprint string `json:"known_fingerprint,omitempty"`
3939
}
4040

41+
type mcpPinRow struct {
42+
Server string `json:"server"`
43+
Fingerprint string `json:"fingerprint"`
44+
}
45+
46+
type mcpPinsResponse struct {
47+
Pins []mcpPinRow `json:"pins"`
48+
}
49+
4150
func mcpPinCheckHandler(d Deps) http.HandlerFunc {
4251
if d.Store == nil || d.PinStorePath == "" {
4352
return todo("mcp.pin.check")
@@ -68,6 +77,32 @@ func mcpPinCheckHandler(d Deps) http.HandlerFunc {
6877
}
6978
}
7079

80+
func mcpPinsListHandler(d Deps) http.HandlerFunc {
81+
if d.PinStorePath == "" {
82+
return todo("mcp.pins.list")
83+
}
84+
return func(w http.ResponseWriter, r *http.Request) {
85+
pins, err := readPins(d.PinStorePath)
86+
if err != nil {
87+
writeError(w, http.StatusInternalServerError, "storage_error", err.Error())
88+
return
89+
}
90+
servers := make([]string, 0, len(pins))
91+
for server := range pins {
92+
servers = append(servers, server)
93+
}
94+
sort.Strings(servers)
95+
rows := make([]mcpPinRow, 0, len(servers))
96+
for _, server := range servers {
97+
rows = append(rows, mcpPinRow{
98+
Server: server,
99+
Fingerprint: pins[server],
100+
})
101+
}
102+
writeJSON(w, http.StatusOK, mcpPinsResponse{Pins: rows})
103+
}
104+
}
105+
71106
func mcpPinAcceptHandler(d Deps) http.HandlerFunc {
72107
if d.Store == nil || d.PinStorePath == "" {
73108
return todo("mcp.pin.accept")

0 commit comments

Comments
 (0)