Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,004 changes: 988 additions & 16 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"lint:eslint:fix": "eslint --fix 'packages/*/src/**/*.ts'",
"format": "prettier --write .",
"format:check": "prettier --check .",
"mcp": "npm run build && node --env-file-if-exists=.env packages/mcp/dist/index.js",
"register-server": "tsx --env-file-if-exists=.env scripts/register-server.ts",
"register-builder": "tsx --env-file-if-exists=.env scripts/register-builder.ts",
"validate": "npm run lint && npm run lint:eslint && npm run format:check && npm test",
Expand Down
41 changes: 41 additions & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@opendatalabs/personal-server-ts-mcp",
"version": "0.0.1",
"description": "MCP (Model Context Protocol) server for the Vana Personal Server",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/vana-com/personal-server-ts.git",
"directory": "packages/mcp"
},
"homepage": "https://github.com/vana-com/personal-server-ts#readme",
"bugs": {
"url": "https://github.com/vana-com/personal-server-ts/issues"
},
"type": "module",
"bin": {
"personal-server-mcp": "./dist/index.js"
},
"main": "./dist/server.js",
"types": "./dist/server.d.ts",
"exports": {
".": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js"
}
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc --build",
"start": "node dist/index.js"
},
"dependencies": {
"@opendatalabs/personal-server-ts-core": "*",
"@modelcontextprotocol/sdk": "^1.27.0"
}
}
70 changes: 70 additions & 0 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env node
import { join } from "node:path";
import pino from "pino";
import { loadConfig } from "@opendatalabs/personal-server-ts-core/config";
import {
resolveRootPath,
DEFAULT_ROOT_PATH,
} from "@opendatalabs/personal-server-ts-core/config";
import {
initializeDatabase,
createIndexManager,
} from "@opendatalabs/personal-server-ts-core/storage/index";
import { createGatewayClient } from "@opendatalabs/personal-server-ts-core/gateway";
import { recoverServerOwner } from "@opendatalabs/personal-server-ts-core/keys";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpServer } from "./server.js";

async function main(): Promise<void> {
const rootPath = process.env.PERSONAL_SERVER_ROOT_PATH;
const config = await loadConfig({ rootPath });

// Logger writes to stderr so stdout stays clean for MCP protocol
const logger = pino({ level: config.logging.level }, pino.destination(2));

// Derive owner from master key signature (required)
const masterKeySignature = process.env.VANA_MASTER_KEY_SIGNATURE as
| `0x${string}`
| undefined;
if (!masterKeySignature) {
logger.error("VANA_MASTER_KEY_SIGNATURE is required for MCP server");
process.exit(1);
}
const serverOwner = await recoverServerOwner(masterKeySignature);
logger.info({ owner: serverOwner }, "MCP server owner derived");

// Initialize data layer (same paths as HTTP server)
const storageRoot = resolveRootPath(rootPath ?? DEFAULT_ROOT_PATH);
const dataDir = join(storageRoot, "data");
const indexPath = join(storageRoot, "index.db");

const db = initializeDatabase(indexPath);
const indexManager = createIndexManager(db);
const hierarchyOptions = { dataDir };
const gatewayClient = createGatewayClient(config.gateway.url);

// Create and start MCP server
const mcpServer = createMcpServer({
indexManager,
hierarchyOptions,
gatewayClient,
serverOwner,
logger,
});

const transport = new StdioServerTransport();
await mcpServer.connect(transport);
logger.info("MCP server connected via stdio");

// Graceful shutdown
process.on("SIGINT", async () => {
await mcpServer.close();
indexManager.close();
process.exit(0);
});
}

main().catch((err) => {
console.error("MCP server failed:", err);
process.exit(1);
});
140 changes: 140 additions & 0 deletions packages/mcp/src/resources/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { readDataFile } from "@opendatalabs/personal-server-ts-core/storage/hierarchy";
import type { McpContext } from "../types.js";

export function registerFilesResources(
server: McpServer,
ctx: McpContext,
): void {
// List all data files (distinct scopes)
server.registerResource(
"files",
"vana://files",
{
title: "All Data Files",
description: "List all data scopes and their latest versions",
mimeType: "application/json",
},
async (uri) => {
const result = ctx.indexManager.listDistinctScopes({
limit: 1000,
});
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result.scopes, null, 2),
},
],
};
},
);

// Get file content by scope
server.registerResource(
"file",
new ResourceTemplate("vana://file/{scope}", {
list: async () => {
const result = ctx.indexManager.listDistinctScopes({
limit: 1000,
});
return {
resources: result.scopes.map((s) => ({
uri: `vana://file/${s.scope}`,
name: s.scope,
description: `Latest version from ${s.latestCollectedAt} (${s.versionCount} versions)`,
mimeType: "application/json",
})),
};
},
}),
{
title: "Data File Content",
description: "Get the latest content of a data file by scope",
mimeType: "application/json",
},
async (uri, params) => {
const scope = params.scope as string;
const entry = ctx.indexManager.findLatestByScope(scope);
if (!entry) {
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: `No data found for scope: ${scope}`,
},
],
};
}
const envelope = await readDataFile(
ctx.hierarchyOptions,
scope,
entry.collectedAt,
);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(envelope, null, 2),
},
],
};
},
);

// File metadata by scope
server.registerResource(
"file-metadata",
new ResourceTemplate("vana://file/{scope}/metadata", {
list: async () => {
const result = ctx.indexManager.listDistinctScopes({
limit: 1000,
});
return {
resources: result.scopes.map((s) => ({
uri: `vana://file/${s.scope}/metadata`,
name: `${s.scope} metadata`,
mimeType: "application/json",
})),
};
},
}),
{
title: "Data File Metadata",
description:
"Get metadata about a data file (versions, size, timestamps)",
mimeType: "application/json",
},
async (uri, params) => {
const scope = params.scope as string;
const entries = ctx.indexManager.findByScope({
scope,
limit: 100,
});
const total = ctx.indexManager.countByScope(scope);
const metadata = {
scope,
totalVersions: total,
versions: entries.map((e) => ({
collectedAt: e.collectedAt,
createdAt: e.createdAt,
sizeBytes: e.sizeBytes,
fileId: e.fileId,
})),
};
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(metadata, null, 2),
},
],
};
},
);
}
29 changes: 29 additions & 0 deletions packages/mcp/src/resources/grants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpContext } from "../types.js";

export function registerGrantsResource(
server: McpServer,
ctx: McpContext,
): void {
server.registerResource(
"grants",
"vana://grants",
{
title: "Active Grants",
description: "List all data access grants for this user",
mimeType: "application/json",
},
async (uri) => {
const grants = await ctx.gatewayClient.listGrantsByUser(ctx.serverOwner);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(grants, null, 2),
},
],
};
},
);
}
41 changes: 41 additions & 0 deletions packages/mcp/src/resources/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpContext } from "../types.js";

export function registerSchemasResource(
server: McpServer,
ctx: McpContext,
): void {
server.registerResource(
"schemas",
"vana://schemas",
{
title: "Available Schemas",
description: "List schemas for all data scopes that have data",
mimeType: "application/json",
},
async (uri) => {
const { scopes } = ctx.indexManager.listDistinctScopes({
limit: 1000,
});
const schemas = await Promise.all(
scopes.map(async (s) => {
try {
const schema = await ctx.gatewayClient.getSchemaForScope(s.scope);
return schema ?? null;
} catch {
return null;
}
}),
);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(schemas.filter(Boolean), null, 2),
},
],
};
},
);
}
29 changes: 29 additions & 0 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpContext } from "./types.js";
import { registerFilesResources } from "./resources/files.js";
import { registerGrantsResource } from "./resources/grants.js";
import { registerSchemasResource } from "./resources/schemas.js";
import { registerListFilesTool } from "./tools/list-files.js";
import { registerGetFileTool } from "./tools/get-file.js";
import { registerSearchFilesTool } from "./tools/search-files.js";

export function createMcpServer(ctx: McpContext): McpServer {
const server = new McpServer({
name: "vana-personal-server",
version: "0.0.1",
});

// Resources
registerFilesResources(server, ctx);
registerGrantsResource(server, ctx);
registerSchemasResource(server, ctx);

// Tools
registerListFilesTool(server, ctx);
registerGetFileTool(server, ctx);
registerSearchFilesTool(server, ctx);

return server;
}

export type { McpContext } from "./types.js";
Loading
Loading