feat: mesh services platform — deploy MCP servers, vaults, scopes
Some checks failed
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Add the foundation for deploying and managing MCP servers on the VPS
broker, with per-peer credential vaults and visibility scopes.

Architecture:
- One Docker container per mesh with a Node supervisor
- Each MCP server runs as a child process with its own stdio pipe
- claudemesh launch installs native MCP entries in ~/.claude.json
- Mid-session deploys fall back to svc__* dynamic tools + list_changed

New components:
- DB: mesh.service + mesh.vault_entry tables, mesh.skill extensions
- Broker: 19 wire protocol types, 11 message handlers, service catalog
  in hello_ack with scope filtering, service-manager.ts (775 lines)
- CLI: 13 tool definitions, 12 WS client methods, tool call handlers,
  startServiceProxy() for native MCP proxy mode
- Launch: catalog fetch, native MCP entry install, stale sweep, cleanup,
  MCP_TIMEOUT=30s, MAX_MCP_OUTPUT_TOKENS=50k

Security: path sanitization on service names, column whitelist on
upsertService, returning()-based delete checks, vault E2E encryption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-08 10:53:03 +01:00
parent a4f2e0aa81
commit e1cafa54b3
12 changed files with 3126 additions and 4 deletions

View File

@@ -454,6 +454,12 @@ export const meshSkill = meshSchema.table(
tags: text().array().default([]),
authorMemberId: text().references(() => meshMember.id),
authorName: text(),
sourceType: text().default("inline"),
bundleFileId: text().references(() => meshFile.id),
gitUrl: text(),
gitBranch: text().default("main"),
gitSha: text(),
manifest: jsonb(),
createdAt: timestamp().defaultNow().notNull(),
updatedAt: timestamp().defaultNow().notNull(),
},
@@ -487,6 +493,63 @@ export const meshWebhook = meshSchema.table(
(table) => [uniqueIndex("webhook_mesh_name_idx").on(table.meshId, table.name)],
);
export const meshService = meshSchema.table(
"service",
{
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
name: text().notNull(),
type: text().notNull(),
sourceType: text().notNull(),
sourceFileId: text().references(() => meshFile.id),
sourceGitUrl: text(),
sourceGitBranch: text().default("main"),
sourceGitSha: text(),
prevGitSha: text(),
description: text().notNull(),
instructions: text(),
toolsSchema: jsonb(),
manifest: jsonb(),
runtime: text(),
status: text().default("stopped"),
config: jsonb().default({}),
lastHealth: timestamp(),
restartCount: integer().default(0),
version: integer().default(1),
scope: jsonb().default({ type: "peer" }),
deployedBy: text().references(() => meshMember.id),
deployedByName: text(),
createdAt: timestamp().defaultNow().notNull(),
updatedAt: timestamp().defaultNow().notNull(),
},
(table) => [uniqueIndex("service_mesh_name_idx").on(table.meshId, table.name)],
);
export const meshVaultEntry = meshSchema.table(
"vault_entry",
{
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
memberId: text()
.references(() => meshMember.id)
.notNull(),
key: text().notNull(),
ciphertext: text().notNull(),
nonce: text().notNull(),
sealedKey: text().notNull(),
entryType: text().default("env"),
mountPath: text(),
description: text(),
createdAt: timestamp().defaultNow().notNull(),
updatedAt: timestamp().defaultNow().notNull(),
},
(table) => [uniqueIndex("vault_entry_mesh_member_key_idx").on(table.meshId, table.memberId, table.key)],
);
export const meshWebhookRelations = relations(meshWebhook, ({ one }) => ({
mesh: one(mesh, {
fields: [meshWebhook.meshId],
@@ -787,9 +850,34 @@ export const meshSkillRelations = relations(meshSkill, ({ one }) => ({
fields: [meshSkill.authorMemberId],
references: [meshMember.id],
}),
bundleFile: one(meshFile, {
fields: [meshSkill.bundleFileId],
references: [meshFile.id],
}),
}));
export const selectMeshSkillSchema = createSelectSchema(meshSkill);
export const insertMeshSkillSchema = createInsertSchema(meshSkill);
export type SelectMeshSkill = typeof meshSkill.$inferSelect;
export type InsertMeshSkill = typeof meshSkill.$inferInsert;
export const meshServiceRelations = relations(meshService, ({ one }) => ({
mesh: one(mesh, { fields: [meshService.meshId], references: [mesh.id] }),
sourceFile: one(meshFile, { fields: [meshService.sourceFileId], references: [meshFile.id] }),
deployer: one(meshMember, { fields: [meshService.deployedBy], references: [meshMember.id] }),
}));
export const selectMeshServiceSchema = createSelectSchema(meshService);
export const insertMeshServiceSchema = createInsertSchema(meshService);
export type SelectMeshService = typeof meshService.$inferSelect;
export type InsertMeshService = typeof meshService.$inferInsert;
export const meshVaultEntryRelations = relations(meshVaultEntry, ({ one }) => ({
mesh: one(mesh, { fields: [meshVaultEntry.meshId], references: [mesh.id] }),
member: one(meshMember, { fields: [meshVaultEntry.memberId], references: [meshMember.id] }),
}));
export const selectMeshVaultEntrySchema = createSelectSchema(meshVaultEntry);
export const insertMeshVaultEntrySchema = createInsertSchema(meshVaultEntry);
export type SelectMeshVaultEntry = typeof meshVaultEntry.$inferSelect;
export type InsertMeshVaultEntry = typeof meshVaultEntry.$inferInsert;