Initial meta MCP server scaffold
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.DS_Store
|
||||
*.log
|
||||
100
README.md
Normal file
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Meta MCP (ContextVM)
|
||||
|
||||
Meta MCP is a ContextVM server that accepts **requirements** for new MCP tools and returns a job id. It is intended to be used by other AIs that only have access to a single "meta" tool. The meta MCP then builds, deploys, and announces a new MCP server via Nostr (ContextVM).
|
||||
|
||||
This repo contains the **server-side** MCP for the meta tool. It currently implements the request + job tracking surface; the build pipeline is stubbed and will be wired to the ContextVM/CVMI deployment flow next.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
- Accept requirements for new tools
|
||||
- Create a repo/code for the new MCP server
|
||||
- Build + run the MCP server on this VPS
|
||||
- Announce the server on Nostr (ContextVM)
|
||||
- Return the **server pubkey** back to the requester
|
||||
|
||||
---
|
||||
|
||||
## Tool API
|
||||
|
||||
### `meta.request_tool`
|
||||
Request a new MCP tool/server.
|
||||
|
||||
**Input**
|
||||
```json
|
||||
{
|
||||
"requirements": [
|
||||
{
|
||||
"name": "get_current_weather_by_city",
|
||||
"description": "Returns current weather for a city",
|
||||
"inputSchema": {"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}
|
||||
}
|
||||
],
|
||||
"timeoutSeconds": 120,
|
||||
"requester": {"name":"weather-bot","pubkey":"npub1..."},
|
||||
"context": {"notes":"Need fast response"}
|
||||
}
|
||||
```
|
||||
|
||||
**Output**
|
||||
```json
|
||||
{
|
||||
"status": "building",
|
||||
"message": "Accepted. Building MCP server (pipeline stub).",
|
||||
"jobId": "<uuid>"
|
||||
}
|
||||
```
|
||||
|
||||
### `meta.job_status`
|
||||
Check a previous job.
|
||||
|
||||
**Input**
|
||||
```json
|
||||
{ "jobId": "<uuid>" }
|
||||
```
|
||||
|
||||
### `meta.jobs_recent`
|
||||
List recent jobs.
|
||||
|
||||
**Input**
|
||||
```json
|
||||
{ "limit": 10 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment (ContextVM/CVMI)
|
||||
|
||||
This server runs via stdio. Use **cvmi serve** to expose it over Nostr.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
cvmi serve -- node dist/server.js
|
||||
```
|
||||
|
||||
You can set `CVMI_SERVE_*` env vars to configure relays, encryption, and keys.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Wire this server to a build/deploy pipeline
|
||||
- Add persistent storage for job state
|
||||
- Auto-create repos + scaffolds for new MCPs
|
||||
- Auto-start new MCP servers and announce via CVMI
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
1377
package-lock.json
generated
Normal file
1377
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "meta-mcp",
|
||||
"version": "0.1.0",
|
||||
"description": "Meta MCP ContextVM: accepts requirements to create and announce new MCP servers over Nostr",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/server.js",
|
||||
"dev": "ts-node --esm src/server.ts",
|
||||
"lint": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
130
src/server.ts
Normal file
130
src/server.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
import { createJob, getJob, listJobs, updateJob } from './storage.js';
|
||||
import type { MetaRequest, MetaResponse } from './types.js';
|
||||
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: 'meta-mcp',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const ToolRequirementSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
inputSchema: z.record(z.unknown()),
|
||||
outputSchema: z.record(z.unknown()).optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
const MetaRequestSchema = z.object({
|
||||
requirements: z.array(ToolRequirementSchema).min(1),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
requester: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
pubkey: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
context: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
'meta.request_tool',
|
||||
{
|
||||
description:
|
||||
'Request creation of a new MCP tool/server. Returns a job id and later the server pubkey when available.',
|
||||
inputSchema: MetaRequestSchema,
|
||||
},
|
||||
async (args) => {
|
||||
const parsed = MetaRequestSchema.parse(args);
|
||||
const job = createJob(parsed as MetaRequest);
|
||||
|
||||
// TODO: Hook into build/deploy pipeline
|
||||
updateJob(job.id, {
|
||||
status: 'building',
|
||||
message: 'Accepted. Building MCP server (pipeline stub).',
|
||||
});
|
||||
|
||||
const response: MetaResponse = {
|
||||
status: 'building',
|
||||
message: 'Accepted. Building MCP server (pipeline stub).',
|
||||
jobId: job.id,
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'meta.job_status',
|
||||
{
|
||||
description: 'Check status of a previously requested MCP tool build.',
|
||||
inputSchema: z.object({ jobId: z.string() }),
|
||||
},
|
||||
async (args) => {
|
||||
const job = getJob(args.jobId);
|
||||
if (!job) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({ error: 'job_not_found', jobId: args.jobId }),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(job.response, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'meta.jobs_recent',
|
||||
{
|
||||
description: 'List recent build jobs for monitoring.',
|
||||
inputSchema: z.object({ limit: z.number().int().positive().optional() }),
|
||||
},
|
||||
async (args) => {
|
||||
const jobs = listJobs().slice(0, args.limit ?? 10).map((job) => job.response);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(jobs, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Meta MCP server failed to start', err);
|
||||
process.exit(1);
|
||||
});
|
||||
41
src/storage.ts
Normal file
41
src/storage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { MetaRequest, MetaResponse } from './types.js';
|
||||
|
||||
export type Job = {
|
||||
id: string;
|
||||
request: MetaRequest;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
response: MetaResponse;
|
||||
};
|
||||
|
||||
const jobs = new Map<string, Job>();
|
||||
|
||||
export function createJob(request: MetaRequest): Job {
|
||||
const id = randomUUID();
|
||||
const now = Date.now();
|
||||
const response: MetaResponse = {
|
||||
status: 'queued',
|
||||
message: 'Request received. Preparing build pipeline.',
|
||||
jobId: id,
|
||||
};
|
||||
const job: Job = { id, request, createdAt: now, updatedAt: now, response };
|
||||
jobs.set(id, job);
|
||||
return job;
|
||||
}
|
||||
|
||||
export function updateJob(id: string, patch: Partial<MetaResponse>): Job | null {
|
||||
const job = jobs.get(id);
|
||||
if (!job) return null;
|
||||
job.updatedAt = Date.now();
|
||||
job.response = { ...job.response, ...patch };
|
||||
return job;
|
||||
}
|
||||
|
||||
export function getJob(id: string): Job | null {
|
||||
return jobs.get(id) ?? null;
|
||||
}
|
||||
|
||||
export function listJobs(): Job[] {
|
||||
return [...jobs.values()].sort((a, b) => b.createdAt - a.createdAt);
|
||||
}
|
||||
27
src/types.ts
Normal file
27
src/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export type ToolRequirement = {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: Record<string, unknown>;
|
||||
outputSchema?: Record<string, unknown>;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
export type MetaRequest = {
|
||||
requirements: ToolRequirement[];
|
||||
timeoutSeconds?: number;
|
||||
requester?: {
|
||||
name?: string;
|
||||
pubkey?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type MetaResponse = {
|
||||
status: 'queued' | 'building' | 'running' | 'failed';
|
||||
message: string;
|
||||
jobId: string;
|
||||
serverPubkey?: string;
|
||||
repoUrl?: string;
|
||||
logs?: string[];
|
||||
};
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user