char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
7a8c6a6
main
1import type { Environment } from "@berry/api" ; 2import { BACKGROUND_CONTEXT , withAbortSignal } from "@earendil-works/chord/context" ; 3 4export const DEFAULT_CONFIG = "~/.config/berry/mcp.json" ; 5 6export type Server = 7& { 8enabled :boolean ; 9timeout :number ; 10cwd :string ; 11} 12& ( 13| { command :string ; args :string []; env :Record < string , string > } 14| { url :string ; headers :Record < string , string > } 15); 16 17function record ( value :unknown ) :value isRecord < string , unknown > { 18return value !== null && typeof value === "object" && ! Array . isArray ( value ); 19} 20 21/** The server entries from the selected file on the conversation's machine. */ 22export async function readConfig ( env :Environment , path :string , signal :AbortSignal ) { 23const result = await env . readTextFile ( path , withAbortSignal ( signal , BACKGROUND_CONTEXT )); 24if ( ! result . ok ) { 25if ( result . error . code === "not_found" ) return {}; 26throw result . error ; 27} 28let parsed :unknown ; 29try { 30parsed = JSON . parse ( result . value ); 31} catch { 32throw new Error ( `Invalid JSON in MCP configuration: ${ path } ` ); 33} 34if ( ! record ( parsed ) || ! record ( parsed . mcpServers )) { 35throw new Error ( `MCP configuration ${ path } must contain an mcpServers object` ); 36} 37return parsed . mcpServers ; 38} 39 40/** Validate a server and resolve variables and paths on its own machine. */ 41export async function serverConfig ( 42env :Environment , 43raw :unknown , 44signal :AbortSignal , 45) :Promise < Server > { 46if ( ! record ( raw )) throw new Error ( "Server configuration must be an object" ); 47if ( raw . enabled === false ) throw new Error ( "MCP server is disabled" ); 48const string = ( value :unknown , field :string ) :string => { 49if ( typeof value !== "string" ) throw new Error ( ` ${ field } must be a string` ); 50return value . replace ( / \$\{([^}]+)\} / g , ( _ , name :string ) => { 51const found = env . variables [ name ]; 52if ( found === undefined ) { 53throw new Error ( `Environment variable ${ name } is not set on ${ env . name } ` ); 54} 55return found ; 56}); 57}; 58const strings = ( value :unknown , field :string ) :Record < string , string > => { 59if ( value === undefined ) return {}; 60if ( ! record ( value )) throw new Error ( ` ${ field } must be an object of strings` ); 61return Object . fromEntries ( 62Object . entries ( value ). map (([ key , value ]) => [ key , string ( value , field )]), 63); 64}; 65const home = ( value :string ) => 66value === "~" ?env . home :/ ^~[/\\] / . test ( value ) ?env . home + value . slice ( 1 ) :value ; 67if ( raw . enabled !== undefined && typeof raw . enabled !== "boolean" ) { 68throw new Error ( "enabled must be a boolean" ); 69} 70const timeout = raw . timeout ?? 60 ; 71if ( typeof timeout !== "number" || ! Number . isFinite ( timeout * 1000 ) || timeout <= 0 ) { 72throw new Error ( "timeout must be a positive number of seconds" ); 73} 74if ( timeout * 1000 > 0x7fffffff ) { 75throw new Error ( "timeout exceeds the maximum supported timer duration" ); 76} 77const cwd = await env . absolutePath ( 78raw . cwd === undefined ?env . cwd :home ( string ( raw . cwd , "cwd" )), 79withAbortSignal ( signal , BACKGROUND_CONTEXT ), 80); 81if ( ! cwd . ok ) throw cwd . error ; 82const base = { enabled :raw . enabled !== false , timeout :timeout * 1000 , cwd :cwd . value }; 83if ( raw . url !== undefined ) { 84if ( raw . command !== undefined ) throw new Error ( "Specify either command or url, not both" ); 85if ( raw . type !== undefined && raw . type !== "http" && raw . type !== "streamable-http" ) { 86throw new Error ( "Only Streamable HTTP is supported for URL servers" ); 87} 88if ( raw . oauth || raw . auth ) { 89throw new Error ( "OAuth is not supported yet; use headers with an access token" ); 90} 91const expanded = string ( raw . url , "url" ); 92let url :URL ; 93try { 94url = new URL ( expanded ); 95} catch { 96throw new Error ( "url must be a valid absolute HTTP or HTTPS URL" ); 97} 98if ( url . protocol !== "https:" && url . protocol !== "http:" ) { 99throw new Error ( "MCP URLs must use HTTP or HTTPS" ); 100} 101if ( url . username || url . password ) { 102throw new Error ( "MCP URL credentials are not supported; use headers instead" ); 103} 104return { 105 ...base , 106url :url . href , 107headers :strings ( raw . headers , "headers" ), 108}; 109} 110if ( raw . type !== undefined && raw . type !== "stdio" ) { 111throw new Error ( "Only stdio command servers are supported" ); 112} 113const command = home ( string ( raw . command , "command" )); 114if ( ! command . trim ()) throw new Error ( "command must not be empty" ); 115if ( raw . args !== undefined && ! Array . isArray ( raw . args )) { 116throw new Error ( "args must be an array of strings" ); 117} 118return { 119 ...base , 120 command, 121args :( raw . args as unknown [] | undefined ?? []). map (( arg ) => home ( string ( arg , "args" ))), 122env :strings ( raw . env , "env" ), 123}; 124}