@lifesi/genesis-mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/genesis-mcp.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.js +63 -0
- package/dist/tools.d.ts +38 -0
- package/dist/tools.js +77 -0
- package/package.json +17 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createServer, createHttpServer, serveStdio } from './server.js';
|
|
2
|
+
export { tools, specTools, metaTools, searchTools, callTool } from './tools.js';
|
|
3
|
+
export async function cli(args) {
|
|
4
|
+
const { serveStdio, createHttpServer } = await import('./server.js');
|
|
5
|
+
if (args[0] === 'http') {
|
|
6
|
+
const port = Number(process.env.PORT ?? args[1] ?? 8787);
|
|
7
|
+
createHttpServer().listen(port, () => console.error(`genesis-mcp listening on http://0.0.0.0:${port}/mcp`));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
await serveStdio();
|
|
11
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import type { GenesisOptions } from '@lifesi/genesis';
|
|
4
|
+
export declare function createServer(opts?: GenesisOptions): Server<{
|
|
5
|
+
method: string;
|
|
6
|
+
params?: {
|
|
7
|
+
[x: string]: unknown;
|
|
8
|
+
_meta?: {
|
|
9
|
+
[x: string]: unknown;
|
|
10
|
+
progressToken?: string | number | undefined;
|
|
11
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
12
|
+
taskId: string;
|
|
13
|
+
} | undefined;
|
|
14
|
+
} | undefined;
|
|
15
|
+
} | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
method: string;
|
|
18
|
+
params?: {
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
_meta?: {
|
|
21
|
+
[x: string]: unknown;
|
|
22
|
+
progressToken?: string | number | undefined;
|
|
23
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
24
|
+
taskId: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
} | undefined;
|
|
27
|
+
} | undefined;
|
|
28
|
+
}, {
|
|
29
|
+
[x: string]: unknown;
|
|
30
|
+
_meta?: {
|
|
31
|
+
[x: string]: unknown;
|
|
32
|
+
progressToken?: string | number | undefined;
|
|
33
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
34
|
+
taskId: string;
|
|
35
|
+
} | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
/** stdio: credential from GENESIS_API_KEY / GENESIS_TOKEN (or `credentials` provider). */
|
|
39
|
+
export declare function serveStdio(opts?: GenesisOptions): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Streamable HTTP (stateless) at POST /mcp, for mcp.life.one.
|
|
42
|
+
* Each request's `Authorization: Bearer <LifeID key | M2M token>` is forwarded as that caller's credential;
|
|
43
|
+
* the server never holds a credential of its own. Requests without one get 401 + WWW-Authenticate.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createHttpServer(opts?: Omit<GenesisOptions, 'apiKey' | 'token' | 'credentials'>): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { callTool, tools } from './tools.js';
|
|
7
|
+
export function createServer(opts = {}) {
|
|
8
|
+
const server = new Server({ name: 'genesis', version: '0.1.0' }, {
|
|
9
|
+
capabilities: { tools: {} },
|
|
10
|
+
instructions: 'Genesis: one API for Foundry (inference), Hives (compute), Relays (DNS) and LifeID keys. Use the named tools for common tasks; use genesis_search_tools + genesis_call for anything else.',
|
|
11
|
+
});
|
|
12
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: tools() }));
|
|
13
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => callTool(opts, req.params.name, (req.params.arguments ?? {})));
|
|
14
|
+
return server;
|
|
15
|
+
}
|
|
16
|
+
/** stdio: credential from GENESIS_API_KEY / GENESIS_TOKEN (or `credentials` provider). */
|
|
17
|
+
export async function serveStdio(opts = {}) {
|
|
18
|
+
await createServer(opts).connect(new StdioServerTransport());
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Streamable HTTP (stateless) at POST /mcp, for mcp.life.one.
|
|
22
|
+
* Each request's `Authorization: Bearer <LifeID key | M2M token>` is forwarded as that caller's credential;
|
|
23
|
+
* the server never holds a credential of its own. Requests without one get 401 + WWW-Authenticate.
|
|
24
|
+
*/
|
|
25
|
+
export function createHttpServer(opts = {}) {
|
|
26
|
+
return http.createServer(async (req, res) => {
|
|
27
|
+
const url = new URL(req.url ?? '/', 'http://x');
|
|
28
|
+
if (url.pathname === '/healthz') {
|
|
29
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end('{"ok":true}');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (url.pathname !== '/mcp') {
|
|
33
|
+
res.writeHead(404).end();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (req.method !== 'POST') {
|
|
37
|
+
res.writeHead(405, { allow: 'POST' }).end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const m = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? '');
|
|
41
|
+
if (!m) {
|
|
42
|
+
res.writeHead(401, { 'content-type': 'application/json', 'www-authenticate': 'Bearer realm="genesis", error="invalid_token"' })
|
|
43
|
+
.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'Send Authorization: Bearer <LifeID API key or M2M token>' }, id: null }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let body = '';
|
|
47
|
+
for await (const chunk of req)
|
|
48
|
+
body += chunk;
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(body);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
res.writeHead(400).end('invalid JSON');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const server = createServer({ ...opts, token: m[1] });
|
|
58
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
59
|
+
res.on('close', () => { transport.close(); server.close(); });
|
|
60
|
+
await server.connect(transport);
|
|
61
|
+
await transport.handleRequest(req, res, parsed);
|
|
62
|
+
});
|
|
63
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type GenesisOptions, type OperationMeta } from '@lifesi/genesis';
|
|
2
|
+
export interface ToolDef {
|
|
3
|
+
name: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: Record<string, unknown>;
|
|
7
|
+
annotations?: {
|
|
8
|
+
readOnlyHint?: boolean;
|
|
9
|
+
destructiveHint?: boolean;
|
|
10
|
+
idempotentHint?: boolean;
|
|
11
|
+
openWorldHint?: boolean;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Operations callable by agents (everything except gateway-internal ones like credential verify). */
|
|
15
|
+
export declare const callable: [string, OperationMeta][];
|
|
16
|
+
/** Curated tools: one per operation tagged x-mcp in the merged spec. */
|
|
17
|
+
export declare const specTools: ToolDef[];
|
|
18
|
+
export declare const metaTools: ToolDef[];
|
|
19
|
+
export declare const tools: () => ToolDef[];
|
|
20
|
+
export declare function searchTools(query: string, service?: string, limit?: number): {
|
|
21
|
+
operation: string;
|
|
22
|
+
service: string;
|
|
23
|
+
method: string;
|
|
24
|
+
path: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
status: string;
|
|
27
|
+
inputSchema: Record<string, unknown>;
|
|
28
|
+
}[];
|
|
29
|
+
type Result = {
|
|
30
|
+
content: {
|
|
31
|
+
type: 'text';
|
|
32
|
+
text: string;
|
|
33
|
+
}[];
|
|
34
|
+
isError?: boolean;
|
|
35
|
+
structuredContent?: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
export declare function callTool(opts: GenesisOptions, name: string, args?: Record<string, unknown>): Promise<Result>;
|
|
38
|
+
export {};
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Genesis, GenesisError, operationTable } from '@lifesi/genesis';
|
|
2
|
+
const all = Object.entries(operationTable);
|
|
3
|
+
/** Operations callable by agents (everything except gateway-internal ones like credential verify). */
|
|
4
|
+
export const callable = all.filter(([, op]) => !op.internal);
|
|
5
|
+
function describe(id, op) {
|
|
6
|
+
const status = op.upstream.status === 'live' ? '' : ` [service status: ${op.upstream.status}]`;
|
|
7
|
+
return `${op.summary} (${op.method} ${op.path})${status}`;
|
|
8
|
+
}
|
|
9
|
+
const annotate = (op) => ({
|
|
10
|
+
readOnlyHint: op.method === 'GET', destructiveHint: op.method === 'DELETE',
|
|
11
|
+
idempotentHint: ['GET', 'PUT', 'DELETE'].includes(op.method), openWorldHint: true,
|
|
12
|
+
});
|
|
13
|
+
/** Curated tools: one per operation tagged x-mcp in the merged spec. */
|
|
14
|
+
export const specTools = callable.filter(([, op]) => op.mcp).map(([id, op]) => ({
|
|
15
|
+
name: id, title: op.summary, description: describe(id, op), inputSchema: op.inputSchema, annotations: annotate(op),
|
|
16
|
+
}));
|
|
17
|
+
export const metaTools = [
|
|
18
|
+
{
|
|
19
|
+
name: 'genesis_search_tools',
|
|
20
|
+
description: `Search all ${callable.length} Genesis API operations (Foundry, Hives, Relays, IAM...) by keyword. Returns operation ids and input schemas to use with genesis_call.`,
|
|
21
|
+
inputSchema: { type: 'object', properties: {
|
|
22
|
+
query: { type: 'string', description: 'keywords, e.g. "dns record" or "deploy"' },
|
|
23
|
+
service: { type: 'string', description: 'optional service filter: foundry | hives | relays | iam' },
|
|
24
|
+
limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
|
|
25
|
+
}, required: ['query'], additionalProperties: false },
|
|
26
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'genesis_call',
|
|
30
|
+
description: 'Call any Genesis API operation by id (find ids with genesis_search_tools). arguments = path params + query params + body.',
|
|
31
|
+
inputSchema: { type: 'object', properties: {
|
|
32
|
+
operation: { type: 'string', description: 'operation id, e.g. hives_list_secrets' },
|
|
33
|
+
arguments: { type: 'object', additionalProperties: true, default: {} },
|
|
34
|
+
}, required: ['operation'], additionalProperties: false },
|
|
35
|
+
annotations: { openWorldHint: true },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
export const tools = () => [...metaTools, ...specTools];
|
|
39
|
+
export function searchTools(query, service, limit = 10) {
|
|
40
|
+
const terms = query.toLowerCase().split(/\W+/).filter(Boolean);
|
|
41
|
+
return callable
|
|
42
|
+
.filter(([, op]) => !service || op.service === service)
|
|
43
|
+
.map(([id, op]) => {
|
|
44
|
+
const hay = `${id} ${op.summary} ${op.path}`.toLowerCase();
|
|
45
|
+
const score = terms.reduce((s, t) => s + (hay.includes(t) ? (id.toLowerCase().includes(t) ? 2 : 1) : 0), 0);
|
|
46
|
+
return { id, op, score };
|
|
47
|
+
})
|
|
48
|
+
.filter((x) => x.score > 0)
|
|
49
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
50
|
+
.slice(0, limit)
|
|
51
|
+
.map(({ id, op }) => ({ operation: id, service: op.service, method: op.method, path: op.path, summary: op.summary, status: op.upstream.status, inputSchema: op.inputSchema }));
|
|
52
|
+
}
|
|
53
|
+
const text = (v, isError = false) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }], ...(isError ? { isError } : {}) });
|
|
54
|
+
async function invoke(genesis, id, args) {
|
|
55
|
+
const op = operationTable[id];
|
|
56
|
+
if (!op || op.internal)
|
|
57
|
+
return text(`Unknown operation "${id}". Use genesis_search_tools to find one.`, true);
|
|
58
|
+
try {
|
|
59
|
+
const r = await genesis.request(id, args);
|
|
60
|
+
return text(r === undefined ? { ok: true } : r);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (e instanceof GenesisError)
|
|
64
|
+
return text({ error: { type: e.type, status: e.status, message: e.message, request_id: e.requestId } }, true);
|
|
65
|
+
return text({ error: { type: 'network', message: e.message, hint: `The ${op.service} service may be unreachable (status: ${op.upstream.status}).` } }, true);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function callTool(opts, name, args = {}) {
|
|
69
|
+
if (name === 'genesis_search_tools')
|
|
70
|
+
return text(searchTools(String(args.query ?? ''), args.service, Number(args.limit ?? 10)));
|
|
71
|
+
const genesis = new Genesis(opts);
|
|
72
|
+
if (name === 'genesis_call')
|
|
73
|
+
return invoke(genesis, String(args.operation ?? ''), args.arguments ?? {});
|
|
74
|
+
if (!specTools.some((t) => t.name === name))
|
|
75
|
+
return text(`Unknown tool "${name}".`, true);
|
|
76
|
+
return invoke(genesis, name, args);
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifesi/genesis-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Genesis MCP server (stdio + streamable HTTP): tools generated from the Genesis OpenAPI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"genesis-mcp": "bin/genesis-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
15
|
+
"@lifesi/genesis": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json"
|
|
19
|
+
}
|
|
6
20
|
}
|
package/README.md
DELETED