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