返回 CodeWhale
mcp.ts
1 /** Definition proposals only. Rust owns the sole MCP catalog, transport and auth. */
2 import { Service, type Context } from '@deepseek-ai/cordis'
3 import type { RpcPeer } from '../rpc.ts'
4 import { isJson } from '../json.ts'
5 import { OwnedRegistrations, type OwnedEntry, type OwnerBase } from './owned.ts'
6 export interface McpDefinition { readonly serverName: string; readonly server: Record<string, unknown> }
7 export interface LocalMcp<O extends OwnerBase = OwnerBase> extends OwnedEntry<O> {}
8 export function normalizeMcp(value: unknown): McpDefinition {
9 if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(k=>k!=='serverName'&&k!=='server')) throw new TypeError('MCP needs serverName and server')
10 const {serverName,server}=value as McpDefinition
11 if (typeof serverName!=='string'||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(serverName)||!server||Array.isArray(server)||typeof server!=='object'||!isJson(server)) throw new TypeError('MCP definition must be bounded plain JSON')
12 // Literal credential-bearing fields must never enter the host protocol.
13 for(const key of ['headers','env']) if(server[key]!==undefined && (!server[key]||typeof server[key]!=='object'||Array.isArray(server[key])||Object.keys(server[key] as object).length)) throw new TypeError('MCP credentials remain under Rust authentication authority')
14 if(typeof server.url==='string') {
15 const url=new URL(server.url)
16 if(url.username||url.password||url.search||url.hash) throw new TypeError('MCP endpoint credentials and query data remain under Rust authority')
17 }
18 if(Buffer.byteLength(JSON.stringify(server))>64*1024) throw new RangeError('MCP definition exceeds 64 KiB')
19 return Object.freeze({serverName,server:structuredClone(server)})
20 }
21 export class McpDefinitions<O extends OwnerBase> {
22 private readonly registrations:OwnedRegistrations<O,LocalMcp<O>>
23 private readonly owners=new Map<O,Map<string,()=>void>>()
24 constructor(rpc:RpcPeer,ownedBy:(owner:O)=>Map<number,LocalMcp<O>>,warn:(message:string,owner:O)=>void) { this.registrations=new OwnedRegistrations(rpc,'mcp_server',ownedBy,warn) }
25 register(owner:O,value:McpDefinition):()=>void {
26 const {serverName,server}=normalizeMcp(value)
27 const definitions=this.owners.get(owner)??new Map<string,()=>void>()
28 if(definitions.has(serverName)) throw new Error('MCP server is already registered in this entry; dispose it first')
29 if(definitions.size>=64) throw new RangeError('MCP entry exceeds 64 servers')
30 const undo=this.registrations.add({owner,name:serverName,disposed:false},{name:serverName,description:JSON.stringify(server)})
31 const dispose=()=>{if(definitions.get(serverName)!==dispose)return;definitions.delete(serverName);if(!definitions.size)this.owners.delete(owner);undo()}
32 definitions.set(serverName,dispose);this.owners.set(owner,definitions);return dispose
33 }
34 forget(owner:O) {for(const dispose of [...(this.owners.get(owner)?.values()??[])])dispose();this.registrations.forget(owner)}
35 }
36 export function defineMcpService<O extends OwnerBase>(host:{ownerOf(ctx:Context):O|undefined,definitions:McpDefinitions<O>}) {
37 class McpShim extends Service {
38 constructor(ctx:Context){super(ctx,'mcp')}
39 registerServer(definition:McpDefinition):()=>void {
40 const owner=host.ownerOf(this.ctx);if(!owner)throw new Error('MCP proposal is outside an extension owner')
41 const normalized=normalizeMcp(definition)
42 return this.ctx.effect(()=>host.definitions.register(owner,normalized),`mcp.registerServer(${JSON.stringify(normalized.serverName)})`)
43 }
44 }
45 Object.freeze(McpShim.prototype);return McpShim
46 }
47
47 lines TYPESCRIPT