Skip to content

Commit 93bdda2

Browse files
volod-vanaclaude
andauthored
feat: add MCP server with OAuth 2.1 authentication (#56)
Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 3a96ea9 commit 93bdda2

20 files changed

Lines changed: 2268 additions & 18 deletions

package-lock.json

Lines changed: 988 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"lint:eslint:fix": "eslint --fix 'packages/*/src/**/*.ts'",
2929
"format": "prettier --write .",
3030
"format:check": "prettier --check .",
31+
"mcp": "npm run build && node --env-file-if-exists=.env packages/mcp/dist/index.js",
3132
"register-server": "tsx --env-file-if-exists=.env scripts/register-server.ts",
3233
"register-builder": "tsx --env-file-if-exists=.env scripts/register-builder.ts",
3334
"validate": "npm run lint && npm run lint:eslint && npm run format:check && npm test",

packages/mcp/package.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@opendatalabs/personal-server-ts-mcp",
3+
"version": "0.0.1",
4+
"description": "MCP (Model Context Protocol) server for the Vana Personal Server",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/vana-com/personal-server-ts.git",
9+
"directory": "packages/mcp"
10+
},
11+
"homepage": "https://github.com/vana-com/personal-server-ts#readme",
12+
"bugs": {
13+
"url": "https://github.com/vana-com/personal-server-ts/issues"
14+
},
15+
"type": "module",
16+
"bin": {
17+
"personal-server-mcp": "./dist/index.js"
18+
},
19+
"main": "./dist/server.js",
20+
"types": "./dist/server.d.ts",
21+
"exports": {
22+
".": {
23+
"types": "./dist/server.d.ts",
24+
"import": "./dist/server.js"
25+
}
26+
},
27+
"publishConfig": {
28+
"access": "public"
29+
},
30+
"files": [
31+
"dist"
32+
],
33+
"scripts": {
34+
"build": "tsc --build",
35+
"start": "node dist/index.js"
36+
},
37+
"dependencies": {
38+
"@opendatalabs/personal-server-ts-core": "*",
39+
"@modelcontextprotocol/sdk": "^1.27.0"
40+
}
41+
}

packages/mcp/src/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env node
2+
import { join } from "node:path";
3+
import pino from "pino";
4+
import { loadConfig } from "@opendatalabs/personal-server-ts-core/config";
5+
import {
6+
resolveRootPath,
7+
DEFAULT_ROOT_PATH,
8+
} from "@opendatalabs/personal-server-ts-core/config";
9+
import {
10+
initializeDatabase,
11+
createIndexManager,
12+
} from "@opendatalabs/personal-server-ts-core/storage/index";
13+
import { createGatewayClient } from "@opendatalabs/personal-server-ts-core/gateway";
14+
import { recoverServerOwner } from "@opendatalabs/personal-server-ts-core/keys";
15+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16+
import { createMcpServer } from "./server.js";
17+
18+
async function main(): Promise<void> {
19+
const rootPath = process.env.PERSONAL_SERVER_ROOT_PATH;
20+
const config = await loadConfig({ rootPath });
21+
22+
// Logger writes to stderr so stdout stays clean for MCP protocol
23+
const logger = pino({ level: config.logging.level }, pino.destination(2));
24+
25+
// Derive owner from master key signature (required)
26+
const masterKeySignature = process.env.VANA_MASTER_KEY_SIGNATURE as
27+
| `0x${string}`
28+
| undefined;
29+
if (!masterKeySignature) {
30+
logger.error("VANA_MASTER_KEY_SIGNATURE is required for MCP server");
31+
process.exit(1);
32+
}
33+
const serverOwner = await recoverServerOwner(masterKeySignature);
34+
logger.info({ owner: serverOwner }, "MCP server owner derived");
35+
36+
// Initialize data layer (same paths as HTTP server)
37+
const storageRoot = resolveRootPath(rootPath ?? DEFAULT_ROOT_PATH);
38+
const dataDir = join(storageRoot, "data");
39+
const indexPath = join(storageRoot, "index.db");
40+
41+
const db = initializeDatabase(indexPath);
42+
const indexManager = createIndexManager(db);
43+
const hierarchyOptions = { dataDir };
44+
const gatewayClient = createGatewayClient(config.gateway.url);
45+
46+
// Create and start MCP server
47+
const mcpServer = createMcpServer({
48+
indexManager,
49+
hierarchyOptions,
50+
gatewayClient,
51+
serverOwner,
52+
logger,
53+
});
54+
55+
const transport = new StdioServerTransport();
56+
await mcpServer.connect(transport);
57+
logger.info("MCP server connected via stdio");
58+
59+
// Graceful shutdown
60+
process.on("SIGINT", async () => {
61+
await mcpServer.close();
62+
indexManager.close();
63+
process.exit(0);
64+
});
65+
}
66+
67+
main().catch((err) => {
68+
console.error("MCP server failed:", err);
69+
process.exit(1);
70+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import { readDataFile } from "@opendatalabs/personal-server-ts-core/storage/hierarchy";
4+
import type { McpContext } from "../types.js";
5+
6+
export function registerFilesResources(
7+
server: McpServer,
8+
ctx: McpContext,
9+
): void {
10+
// List all data files (distinct scopes)
11+
server.registerResource(
12+
"files",
13+
"vana://files",
14+
{
15+
title: "All Data Files",
16+
description: "List all data scopes and their latest versions",
17+
mimeType: "application/json",
18+
},
19+
async (uri) => {
20+
const result = ctx.indexManager.listDistinctScopes({
21+
limit: 1000,
22+
});
23+
return {
24+
contents: [
25+
{
26+
uri: uri.href,
27+
mimeType: "application/json",
28+
text: JSON.stringify(result.scopes, null, 2),
29+
},
30+
],
31+
};
32+
},
33+
);
34+
35+
// Get file content by scope
36+
server.registerResource(
37+
"file",
38+
new ResourceTemplate("vana://file/{scope}", {
39+
list: async () => {
40+
const result = ctx.indexManager.listDistinctScopes({
41+
limit: 1000,
42+
});
43+
return {
44+
resources: result.scopes.map((s) => ({
45+
uri: `vana://file/${s.scope}`,
46+
name: s.scope,
47+
description: `Latest version from ${s.latestCollectedAt} (${s.versionCount} versions)`,
48+
mimeType: "application/json",
49+
})),
50+
};
51+
},
52+
}),
53+
{
54+
title: "Data File Content",
55+
description: "Get the latest content of a data file by scope",
56+
mimeType: "application/json",
57+
},
58+
async (uri, params) => {
59+
const scope = params.scope as string;
60+
const entry = ctx.indexManager.findLatestByScope(scope);
61+
if (!entry) {
62+
return {
63+
contents: [
64+
{
65+
uri: uri.href,
66+
mimeType: "text/plain",
67+
text: `No data found for scope: ${scope}`,
68+
},
69+
],
70+
};
71+
}
72+
const envelope = await readDataFile(
73+
ctx.hierarchyOptions,
74+
scope,
75+
entry.collectedAt,
76+
);
77+
return {
78+
contents: [
79+
{
80+
uri: uri.href,
81+
mimeType: "application/json",
82+
text: JSON.stringify(envelope, null, 2),
83+
},
84+
],
85+
};
86+
},
87+
);
88+
89+
// File metadata by scope
90+
server.registerResource(
91+
"file-metadata",
92+
new ResourceTemplate("vana://file/{scope}/metadata", {
93+
list: async () => {
94+
const result = ctx.indexManager.listDistinctScopes({
95+
limit: 1000,
96+
});
97+
return {
98+
resources: result.scopes.map((s) => ({
99+
uri: `vana://file/${s.scope}/metadata`,
100+
name: `${s.scope} metadata`,
101+
mimeType: "application/json",
102+
})),
103+
};
104+
},
105+
}),
106+
{
107+
title: "Data File Metadata",
108+
description:
109+
"Get metadata about a data file (versions, size, timestamps)",
110+
mimeType: "application/json",
111+
},
112+
async (uri, params) => {
113+
const scope = params.scope as string;
114+
const entries = ctx.indexManager.findByScope({
115+
scope,
116+
limit: 100,
117+
});
118+
const total = ctx.indexManager.countByScope(scope);
119+
const metadata = {
120+
scope,
121+
totalVersions: total,
122+
versions: entries.map((e) => ({
123+
collectedAt: e.collectedAt,
124+
createdAt: e.createdAt,
125+
sizeBytes: e.sizeBytes,
126+
fileId: e.fileId,
127+
})),
128+
};
129+
return {
130+
contents: [
131+
{
132+
uri: uri.href,
133+
mimeType: "application/json",
134+
text: JSON.stringify(metadata, null, 2),
135+
},
136+
],
137+
};
138+
},
139+
);
140+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import type { McpContext } from "../types.js";
3+
4+
export function registerGrantsResource(
5+
server: McpServer,
6+
ctx: McpContext,
7+
): void {
8+
server.registerResource(
9+
"grants",
10+
"vana://grants",
11+
{
12+
title: "Active Grants",
13+
description: "List all data access grants for this user",
14+
mimeType: "application/json",
15+
},
16+
async (uri) => {
17+
const grants = await ctx.gatewayClient.listGrantsByUser(ctx.serverOwner);
18+
return {
19+
contents: [
20+
{
21+
uri: uri.href,
22+
mimeType: "application/json",
23+
text: JSON.stringify(grants, null, 2),
24+
},
25+
],
26+
};
27+
},
28+
);
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import type { McpContext } from "../types.js";
3+
4+
export function registerSchemasResource(
5+
server: McpServer,
6+
ctx: McpContext,
7+
): void {
8+
server.registerResource(
9+
"schemas",
10+
"vana://schemas",
11+
{
12+
title: "Available Schemas",
13+
description: "List schemas for all data scopes that have data",
14+
mimeType: "application/json",
15+
},
16+
async (uri) => {
17+
const { scopes } = ctx.indexManager.listDistinctScopes({
18+
limit: 1000,
19+
});
20+
const schemas = await Promise.all(
21+
scopes.map(async (s) => {
22+
try {
23+
const schema = await ctx.gatewayClient.getSchemaForScope(s.scope);
24+
return schema ?? null;
25+
} catch {
26+
return null;
27+
}
28+
}),
29+
);
30+
return {
31+
contents: [
32+
{
33+
uri: uri.href,
34+
mimeType: "application/json",
35+
text: JSON.stringify(schemas.filter(Boolean), null, 2),
36+
},
37+
],
38+
};
39+
},
40+
);
41+
}

packages/mcp/src/server.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import type { McpContext } from "./types.js";
3+
import { registerFilesResources } from "./resources/files.js";
4+
import { registerGrantsResource } from "./resources/grants.js";
5+
import { registerSchemasResource } from "./resources/schemas.js";
6+
import { registerListFilesTool } from "./tools/list-files.js";
7+
import { registerGetFileTool } from "./tools/get-file.js";
8+
import { registerSearchFilesTool } from "./tools/search-files.js";
9+
10+
export function createMcpServer(ctx: McpContext): McpServer {
11+
const server = new McpServer({
12+
name: "vana-personal-server",
13+
version: "0.0.1",
14+
});
15+
16+
// Resources
17+
registerFilesResources(server, ctx);
18+
registerGrantsResource(server, ctx);
19+
registerSchemasResource(server, ctx);
20+
21+
// Tools
22+
registerListFilesTool(server, ctx);
23+
registerGetFileTool(server, ctx);
24+
registerSearchFilesTool(server, ctx);
25+
26+
return server;
27+
}
28+
29+
export type { McpContext } from "./types.js";

0 commit comments

Comments
 (0)