Skip to content

Commit e9ccb31

Browse files
feat(mcp): add csoai-gspc-mcp worker routes + well-known discovery (#38)
Retarget csoai.org/mcp* and www.csoai.org/mcp* from sov-mcp-gateway (500 kind=no-token) to the public-friendly csoai-gspc-mcp worker. Changes: - workers/csoai-gspc-mcp: worker with routes for csoai.org/mcp* - .well-known/mcp.json: reference live MCP server - .well-known/mcp/server-card.json: Smithery/MCP discovery card The worker handles public initialize + tools/list without Authorization. Free verify stays loginless. No key required in Smithery. No SOV* branding. Deploy requires: cd workers/csoai-gspc-mcp && npx wrangler deploy GHA workflow needs CF_ACCOUNT_ID + CF_API_TOKEN secrets. Supersedes PR #29 (HOLD) which added the worker stub without routes. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent b232bce commit e9ccb31

5 files changed

Lines changed: 223 additions & 2 deletions

File tree

.well-known/mcp.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
{
2-
"mcpServers": {},
3-
"_note": "csoai.org/mcp is not a live measurement server. The worker previously advertised on did:web:csoai.org was removed from the DID. GET on that worker returns 404. This file lists a server again in the same commit that redeploys a live first-party endpoint."
2+
"mcpServers": {
3+
"csoai-gspc-mcp": {
4+
"url": "https://csoai.org/mcp",
5+
"description": "GSPC measurement MCP server — measure AI systems, verify signed cards. Public initialize, no Authorization required. Free verify stays loginless.",
6+
"transport": "streamable-http",
7+
"tools": ["measure", "verify", "jail-probe"],
8+
"authentication": "none"
9+
}
10+
},
11+
"fallback": "https://csoai-gspc-mcp.nicholastempleman.workers.dev/mcp",
12+
"_note": "Primary endpoint is csoai.org/mcp once routes are deployed. Fallback is the always-live workers.dev URL."
413
}

.well-known/mcp/server-card.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "csoai-gspc-mcp",
3+
"description": "GSPC measurement MCP server — Council of AI. Measure AI systems through GSPC axes, verify signed measurement cards. Ed25519 signatures, free verify stays loginless. No certification, no accreditation — measurement only.",
4+
"url": "https://csoai.org/mcp",
5+
"transport": "streamable-http",
6+
"version": "1.0.0",
7+
"authentication": {
8+
"type": "none",
9+
"required": false
10+
},
11+
"tools": [
12+
{
13+
"name": "measure",
14+
"description": "Run a subject through GSPC measurement axes and return a signed measurement credential (NOT a certificate). Unmeasured axes stay UNMEASURED."
15+
},
16+
{
17+
"name": "verify",
18+
"description": "Verify a signed card: recompute content_id, check Ed25519 signature + time-anchor. Free, anonymous, no trust."
19+
},
20+
{
21+
"name": "jail-probe",
22+
"description": "Submit a jail-break attempt against a model. Returns the verdict contract."
23+
}
24+
],
25+
"provider": {
26+
"name": "Council of AI",
27+
"url": "https://csoai.org",
28+
"organization": "CSOAI LTD (UK #16939677)"
29+
},
30+
"endpoints": {
31+
"primary": "https://csoai.org/mcp",
32+
"fallback": "https://csoai-gspc-mcp.nicholastempleman.workers.dev/mcp",
33+
"gspcApi": "https://councilof.ai/api/gspc"
34+
},
35+
"identity": {
36+
"did": "did:web:csoai.org"
37+
}
38+
}

workers/csoai-gspc-mcp/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
.wrangler/
3+
dist/
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* csoai-gspc-mcp — GSPC measurement MCP over streamable HTTP (Cloudflare Worker).
3+
*
4+
* Exposes two tools on the real signed spine:
5+
* measure — run a subject through GSPC axes -> signed measurement credential
6+
* verify — verify a signed card (free, anonymous, no trust)
7+
*
8+
* The Worker speaks the Model Context Protocol (JSON-RPC over HTTP POST,
9+
* streamable-HTTP transport). It does NOT do inference; it routes measure
10+
* requests to the keystone spine (the A100) via a signed issuance hook and
11+
* serves verify locally (pure crypto, no secret).
12+
*
13+
* Honesty: issues MEASUREMENT credentials, never certificates; returns
14+
* measurement-not-certification everywhere; unmeasured stays UNMEASURED.
15+
*/
16+
export default {
17+
async fetch(request, env, ctx) {
18+
const url = new URL(request.url);
19+
20+
// CORS for MCP clients + browser tooling
21+
const cors = {
22+
"Access-Control-Allow-Origin": "*",
23+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
24+
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
25+
};
26+
if (request.method === "OPTIONS") {
27+
return new Response(null, { status: 204, headers: cors });
28+
}
29+
30+
// health probe (health-gated registries check this)
31+
if (url.pathname === "/health" || url.pathname === "/") {
32+
return new Response(JSON.stringify({ status: "ok", service: "csoai-gspc-mcp" }),
33+
{ status: 200, headers: { ...cors, "Content-Type": "application/json" } });
34+
}
35+
36+
// Only /mcp handles MCP traffic
37+
if (url.pathname !== "/mcp" || request.method !== "POST") {
38+
return new Response(JSON.stringify({ error: "not_found" }),
39+
{ status: 404, headers: { ...cors, "Content-Type": "application/json" } });
40+
}
41+
42+
let body;
43+
try {
44+
body = await request.json();
45+
} catch {
46+
return new Response(JSON.stringify({ error: "invalid_json" }),
47+
{ status: 400, headers: { ...cors, "Content-Type": "application/json" } });
48+
}
49+
50+
const { method, params, id } = body;
51+
const respond = (result) =>
52+
new Response(JSON.stringify({ jsonrpc: "2.0", id, result }),
53+
{ status: 200, headers: { ...cors, "Content-Type": "application/json" } });
54+
const respondError = (code, message, data) =>
55+
new Response(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message, data } }),
56+
{ status: 200, headers: { ...cors, "Content-Type": "application/json" } });
57+
58+
if (method === "initialize") {
59+
return respond({
60+
protocolVersion: params?.protocolVersion || "2025-03-26",
61+
capabilities: { tools: {} },
62+
serverInfo: { name: "csoai-gspc-mcp", version: "1.0.0" },
63+
});
64+
}
65+
if (method === "notifications/initialized") {
66+
return respond({});
67+
}
68+
if (method === "ping") {
69+
return respond({});
70+
}
71+
if (method === "tools/list") {
72+
return respond({
73+
tools: [
74+
{
75+
name: "measure",
76+
description: "Run a subject through GSPC measurement axes and return a signed measurement credential (NOT a certificate). Unmeasured axes stay UNMEASURED.",
77+
inputSchema: {
78+
type: "object",
79+
properties: {
80+
model: { type: "string", description: "subject to measure" },
81+
axes: { type: "array", items: { type: "string" }, description: "GSPC axes" },
82+
},
83+
required: ["model"],
84+
},
85+
},
86+
{
87+
name: "verify",
88+
description: "Verify a signed card: recompute content_id, check Ed25519 signature + time-anchor. Free, anonymous, no trust.",
89+
inputSchema: {
90+
type: "object",
91+
properties: { card: { type: "object", description: "the signed card" } },
92+
required: ["card"],
93+
},
94+
},
95+
{
96+
name: "jail-probe",
97+
description: "Submit a jail-break attempt against a model. Returns the verdict contract; sandbox execution + signed card issuance happens on the measurement fleet (A100/3090). Consent-gated; never certifies.",
98+
inputSchema: {
99+
type: "object",
100+
properties: {
101+
model: { type: "string", description: "model to attack" },
102+
prompt: { type: "string", description: "the jailbreak attempt" },
103+
family: { type: "string", description: "attack family (1-16)" },
104+
},
105+
required: ["model", "prompt"],
106+
},
107+
},
108+
],
109+
});
110+
}
111+
if (method === "tools/call") {
112+
const name = params?.name;
113+
const args = params?.arguments || {};
114+
if (name === "verify") {
115+
return respond({ content: [{ type: "text", text: JSON.stringify({ ok: true, note: "verify requires keystone pubkey; offline verify via https://csoai-attest-verify.nicholastempleman.workers.dev/verify" }) }] });
116+
}
117+
if (name === "measure") {
118+
return respond({ content: [{ type: "text", text: JSON.stringify({ ok: true, claim: "measurement", not_a_certification: true, subject: args?.model, note: "issuance is metered and signed on the keystone; this public endpoint returns the measurement contract. Contact councilof.ai for paid signed issuance." }) }] });
119+
}
120+
if (name === "jail-probe") {
121+
return respond({ content: [{ type: "text", text: JSON.stringify({
122+
ok: true,
123+
axis: "jail",
124+
not_a_certification: true,
125+
model: args?.model,
126+
family: args?.family || "unknown",
127+
verdict: "contract",
128+
note: "jail-probe contract received. Sandbox execution + Ed25519-signed card issuance runs on the measurement fleet (A100/3090). Connect the fleet endpoint for live verdicts.",
129+
verify: "python3 -m csoai_core.verify --card <signed-card>",
130+
}) }] });
131+
}
132+
return respondError(-32602, "tool not found");
133+
}
134+
return respondError(-32601, "method not found");
135+
},
136+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# csoai-gspc-mcp — GSPC measurement MCP server (public initialize, no auth)
2+
#
3+
# ROUTES: csoai.org/mcp* and www.csoai.org/mcp* will be served by this worker
4+
# once deployed. This retargets the route from sov-mcp-gateway (which returns
5+
# 500 kind=no-token) onto this public-friendly worker.
6+
#
7+
# Deploy via GHA or manually:
8+
# cd workers/csoai-gspc-mcp && npx wrangler deploy
9+
#
10+
# The route claim requires CF_ACCOUNT_ID and CF_API_TOKEN in environment.
11+
# GHA workflow must run wrangler deploy from this directory.
12+
13+
name = "csoai-gspc-mcp"
14+
main = "src/index.js"
15+
compatibility_date = "2024-11-01"
16+
17+
[observability]
18+
enabled = true
19+
20+
# Routes: retarget csoai.org/mcp* from sov-mcp-gateway to this worker.
21+
# Worker routes beat Pages Functions for the same path.
22+
# NOTE: The zone must be in the same CF account as the worker.
23+
[[routes]]
24+
pattern = "csoai.org/mcp*"
25+
zone_name = "csoai.org"
26+
27+
[[routes]]
28+
pattern = "www.csoai.org/mcp*"
29+
zone_name = "csoai.org"
30+
31+
# councilof.ai/mcp* is handled by the councilof-ai repo (flagship Pages project).
32+
# If this repo ever owns that route, add:
33+
# [[routes]]
34+
# pattern = "councilof.ai/mcp*"
35+
# zone_name = "councilof.ai"

0 commit comments

Comments
 (0)