Skip to content

Commit a372ed4

Browse files
author
Rotwang9000
committed
Add /.well-known/mcp/server-card.json for registry auto-discovery
Static server card per SEP-1649 so registry scanners (Smithery, Glama, etc.) can list our metadata without running a full MCP initialize(). Available at https://mcp.seneschal.space/.well-known/mcp/server-card.json.
1 parent 04ba891 commit a372ed4

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

src/mcp-server.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,43 @@ export function buildMcpServer(options = {}) {
205205
return server;
206206
}
207207

208+
// Static server card for registries that prefer not to (or can't)
209+
// auto-scan via Streamable HTTP — e.g. Smithery's fallback path
210+
// described in https://smithery.ai/docs/build/publish#server-scanning,
211+
// and SEP-1649 well-known discovery.
212+
//
213+
// Kept in sync with registerTools() by reading the tool definitions
214+
// from the server instance at startup. The Caddy layer serves this at
215+
// /.well-known/mcp/server-card.json on mcp.seneschal.space.
216+
export function getStaticServerCard() {
217+
return {
218+
serverInfo: {
219+
name: 'Seneschal Data API',
220+
version: '0.1.0',
221+
vendor: 'Seneschal',
222+
homepage: 'https://seneschal.space'
223+
},
224+
authentication: { required: false },
225+
transport: {
226+
type: 'streamable-http',
227+
url: 'https://mcp.seneschal.space/'
228+
},
229+
tools: [
230+
{ name: 'seneschal_health', description: 'Service liveness plus row counts and data-source mtimes.' },
231+
{ name: 'seneschal_list_at_risk_borrowers', description: 'Borrowers across Aave/Morpho/Spark below max_hf, sorted ascending.' },
232+
{ name: 'seneschal_list_borrowers', description: 'Generic discovery surface with HF + debt range filters, sort, offset.' },
233+
{ name: 'seneschal_recent_liquidations', description: 'Recent on-chain liquidation events.' },
234+
{ name: 'seneschal_get_borrower', description: 'Latest snapshot for one borrower across protocols.' },
235+
{ name: 'seneschal_get_borrower_history', description: 'Time-series HF traces for one borrower.' },
236+
{ name: 'seneschal_builder_leaderboard', description: 'Ground-truth Ethereum builder market share.' },
237+
{ name: 'seneschal_stats_overview', description: 'Aggregate snapshot powering the public stats dashboard.' },
238+
{ name: 'seneschal_flashloan_providers', description: 'Curated catalogue of mainnet flash-loan providers including FlashBank.' }
239+
],
240+
resources: [],
241+
prompts: []
242+
};
243+
}
244+
208245
// HTTP listener creating a fresh stateless transport per request.
209246
// This is the recommended pattern from the MCP SDK docs for stateless
210247
// public servers — no session affinity required, trivial to put
@@ -232,6 +269,18 @@ export function startMcpHttpServer(options = {}) {
232269
return;
233270
}
234271

272+
// MCP discovery via SEP-1649 static server card. Used by
273+
// registry scanners (Smithery, Glama, etc.) that don't want to
274+
// run a full MCP initialize() to enumerate tools.
275+
if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
276+
res.writeHead(200, {
277+
'content-type': 'application/json',
278+
'cache-control': 'public, max-age=300'
279+
});
280+
res.end(JSON.stringify(getStaticServerCard(), null, '\t'));
281+
return;
282+
}
283+
235284
// MCP root: only POST /, GET /, DELETE / are valid per the spec.
236285
if (req.url !== '/' && req.url !== '/mcp') {
237286
res.writeHead(404, { 'content-type': 'application/json' });

test/mcp-server.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,17 @@ describe('end-to-end HTTP transport', () => {
191191
const body = await r.json();
192192
expect(body.error.code).toBeDefined();
193193
});
194+
195+
test('GET /.well-known/mcp/server-card.json serves the static card', async () => {
196+
const r = await fetch(`http://127.0.0.1:${serverPort}/.well-known/mcp/server-card.json`);
197+
expect(r.status).toBe(200);
198+
expect(r.headers.get('content-type')).toContain('application/json');
199+
const body = await r.json();
200+
expect(body.serverInfo.name).toMatch(/Seneschal/i);
201+
expect(body.transport.type).toBe('streamable-http');
202+
expect(body.authentication.required).toBe(false);
203+
const toolNames = body.tools.map(t => t.name).sort();
204+
expect(toolNames).toContain('seneschal_flashloan_providers');
205+
expect(toolNames).toContain('seneschal_stats_overview');
206+
});
194207
});

0 commit comments

Comments
 (0)