@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
54 lines (48 loc) • 16.8 kB
JavaScript
import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url);
import{McpOAuthCredentialStore,McpServerConnection,McpServerLog,McpSignInCancelledError,createDefaultTransport,signInMcpServer}from"./chunk-FMPAMX2W.js";import{ModelRuntime,ProjectTrustStore,addMcpServerConfig,loadMcpConfig,openBrowser,questionTerminal,removeMcpServerConfig}from"./chunk-MMDXLZVS.js";import"./chunk-VKJQ6CME.js";import{getMcpToolExposure,validateMcpServerConfig}from"./chunk-4KSTFUIK.js";import"./chunk-Y55SJART.js";import{source_default}from"./chunk-6QGV3IWD.js";import{APP_NAME,CONFIG_DIR_NAME}from"./chunk-36QC74V4.js";import"./chunk-IEBK4HQA.js";import"./chunk-GFUN2N5K.js";import"./chunk-ZG74NES2.js";import"./chunk-TMTOQ7H7.js";import"./chunk-FFMDRXC4.js";import"./chunk-7DDKXS2Q.js";import"./chunk-AUUCDC7R.js";import"./chunk-LPXNMA2W.js";import"./chunk-BAC3QPNW.js";import"./chunk-JXWJOIH4.js";import{existsSync}from"node:fs";import{join}from"node:path";var HELP=`${source_default.bold("Usage:")}
${APP_NAME} mcp add <server> [options] -- <command> [args...]
${APP_NAME} mcp add <server> [options] --url <url>
${APP_NAME} mcp remove <server> [-l]
${APP_NAME} mcp list [--json]
${APP_NAME} mcp login <server> [-l] [--timeout <seconds>]
${APP_NAME} mcp logout <server> [-l]
Configure and check MCP servers and sign in to OAuth servers without starting a session.
Reads ~/${CONFIG_DIR_NAME}/agent/mcp.json and, in trusted projects, ${CONFIG_DIR_NAME}/mcp.json.
Commands:
add <server> Add or replace a server in mcp.json
remove <server> Remove a server from mcp.json
list Show state, tools, and errors (exits 1 on failure)
login <server> Sign in through the browser
logout <server> Delete the stored OAuth credentials
Options for add, remove, login, and logout:
-l, --local Use ${CONFIG_DIR_NAME}/mcp.json in the current project instead of the global file
Options for add:
--url <url> Streamable HTTP server URL (instead of a command)
--env <KEY=VALUE> Environment variable for a stdio server (repeatable)
--cwd <dir> Working directory for a stdio server
--header <KEY=VALUE> HTTP header (repeatable)
--bearer-token-env-var <NAME>
Send "Authorization: Bearer \${NAME}"
--oauth-client-id <id> Pre-registered OAuth client id
--oauth-client-secret <secret>
OAuth client secret (may be \${NAME} or !command)
--oauth-callback-port <port>
Fixed OAuth callback port
--oauth-client-name <name>
Client name sent when registering with the OAuth server
--oauth-client-registration <mode>
auto (default), cimd, or dcr
--exposure <mode> codemode (default), deferred, direct, or hidden
--description <text> What the server offers, shown in the system prompt
Other options:
--json Print the list as JSON
--timeout <seconds> Time limit for the whole sign-in (default: 300)`,HELP_HINT=source_default.dim(`Use "${APP_NAME} mcp --help" for usage.`),DEFAULT_LOGIN_TIMEOUT_SECONDS=300;function errorMessage(error){return error instanceof Error?error.message:String(error)}function describeTransport(entry){let{config}=entry;return"url"in config?config.url:[config.command,...config.args??[]].join(" ")}function getAuthProvider(entry){return"url"in entry.config?entry.config.auth?.provider:void 0}function createConnection(entry,options,credentials,providerToken){return new McpServerConnection({entry,cwd:options.cwd,createTransport:createDefaultTransport,credentials,providerToken,log:new McpServerLog(join(options.agentDir,"mcp.log")),onTools:()=>{}})}function createProviderTokenReader(agentDir){let runtime;return async provider=>{runtime??=ModelRuntime.create({authPath:join(agentDir,"auth.json"),modelsPath:join(agentDir,"models.json"),refreshOnCreate:!1});try{return(await(await runtime).getAuth(provider))?.auth.apiKey}catch{return}}}var OPTION_ALIASES=new Map([["-l","--local"]]);function parseOptions(args,known,error,maxPositionals=Number.POSITIVE_INFINITY){let positional=[],values=new Map,lists=new Map;for(let index=0;index<args.length;index++){let typed=args[index],arg=OPTION_ALIASES.get(typed)??typed;if(arg==="--"||positional.length>=maxPositionals){positional.push(...args.slice(arg==="--"?index+1:index));break}if(!arg.startsWith("--")){positional.push(arg);continue}let name=arg.slice(2),kind=known[name];if(!kind){error(`Unknown option ${typed}.
${HELP_HINT}`);return}if(kind==="flag"){values.set(name,!0);continue}let value=args[++index];if(value===void 0){error(`${arg} needs a value.`);return}kind==="list"?lists.set(name,[...lists.get(name)??[],value]):values.set(name,value)}return{positional,values,lists}}async function runMcpCommand(args,options){let log=options.log??(line=>console.log(line)),error=options.error??(line=>console.error(line)),[command,...rest]=args;if(command===void 0||command==="help"||args.includes("--help")||args.includes("-h"))return log(HELP),0;let projectConfig=join(options.cwd,CONFIG_DIR_NAME,"mcp.json");if(command==="add"||command==="remove")return command==="add"?add(rest,projectConfig,options,log,error):remove(rest,projectConfig,options,log,error);let projectTrusted=new ProjectTrustStore(options.agentDir).get(options.cwd)===!0,loaded=loadMcpConfig({agentDir:options.agentDir,cwd:options.cwd,projectTrusted}),untrustedNote=!projectTrusted&&existsSync(projectConfig)?`${projectConfig} is ignored because the project is not trusted. Start ${APP_NAME} in the project to trust it.`:void 0,credentials=options.credentials??new McpOAuthCredentialStore,providerToken=options.providerToken??createProviderTokenReader(options.agentDir);switch(command){case"list":{let parsed=parseOptions(rest,{json:"flag"},error);return parsed?parsed.positional.length>0?(error(`Usage: ${APP_NAME} mcp list [--json]
${HELP_HINT}`),1):list(loaded,parsed.values.has("json"),untrustedNote,options,credentials,providerToken,log):1}case"login":case"logout":{let parsed=parseOptions(rest,command==="login"?{local:"flag",timeout:"value"}:{local:"flag"},error);if(!parsed)return 1;let[name,...extra]=parsed.positional;if(!name||extra.length>0)return error(`Usage: ${APP_NAME} mcp ${command} <server> [-l]
${HELP_HINT}`),1;let entry=loaded.servers.find(server=>server.name===name);if(!entry){let names=loaded.servers.map(server=>server.name),suggestion=names.find(candidate=>editDistance(candidate,name)<=2);return error(`No MCP server named "${name}".${suggestion?` Did you mean "${suggestion}"?`:""}${untrustedNote?` ${untrustedNote}`:""} Configured: ${names.join(", ")||"none"}.`),1}if(parsed.values.has("local")&&entry.scope!=="project")return error(`MCP server "${name}" is defined in ${entry.source}, not in the project; omit --local.`),1;let authProvider=getAuthProvider(entry);if(authProvider)return error(`MCP server "${name}" uses the credentials of provider "${authProvider}". Run "${APP_NAME} auth ${command} ${authProvider}".`),1;let connection=createConnection(entry,options,credentials,providerToken),url=connection.oauthUrl;if(!url)return error(`MCP server "${name}" does not use OAuth. Only HTTP servers without an Authorization header do.`),1;if(command==="logout"){let removed=credentials.remove(name,url);return log(removed?`Signed out of MCP server "${name}".`:`No stored credentials for MCP server "${name}".`),0}let timeout=Number(parsed.values.get("timeout")??DEFAULT_LOGIN_TIMEOUT_SECONDS);if(!Number.isFinite(timeout)||timeout<=0)return error("--timeout must be a positive number of seconds."),1;try{return await login(entry,connection,url,timeout*1e3,options,credentials,log,error)}finally{await connection.close()}}default:return error(`Unknown mcp command "${command}".
${HELP_HINT}`),1}}function editDistance(a,b){let previous=Array.from({length:b.length+1},(_,index)=>index);for(let i=1;i<=a.length;i++){let current=[i];for(let j=1;j<=b.length;j++)current[j]=Math.min(previous[j]+1,current[j-1]+1,previous[j-1]+(a[i-1]===b[j-1]?0:1));previous=current}return previous[b.length]}function parsePairs(option,pairs,error){if(!pairs)return{};let record={};for(let pair of pairs){let separator=pair.indexOf("=");if(separator<=0){error(`--${option} expects KEY=VALUE, got "${pair}".`);return}record[pair.slice(0,separator)]=pair.slice(separator+1)}return record}function add(args,projectConfig,options,log,error){let usage=`Usage: ${APP_NAME} mcp add <server> [options] (--url <url> | -- <command> [args...])
${HELP_HINT}`,parsed=parseOptions(args,{local:"flag",url:"value",env:"list",cwd:"value",header:"list","bearer-token-env-var":"value","oauth-client-id":"value","oauth-client-secret":"value","oauth-callback-port":"value","oauth-client-name":"value","oauth-client-registration":"value",exposure:"value",description:"value"},error,2);if(!parsed)return 1;let{positional,values,lists}=parsed,[name,...command]=positional,url=values.get("url");if(!name||url===void 0==(command.length===0))return error(usage),1;let value=option=>{let found=values.get(option);return typeof found=="string"?found:void 0},exposure=value("exposure"),misplaced=(url===void 0?["header","bearer-token-env-var","oauth-client-id","oauth-client-secret","oauth-callback-port","oauth-client-name","oauth-client-registration"]:["env","cwd"]).find(option=>values.has(option)||lists.has(option));if(misplaced)return error(`--${misplaced} only applies to ${url===void 0?"HTTP servers (--url)":"stdio servers"}.`),1;let config;if(typeof url=="string"){let headers=parsePairs("header",lists.get("header"),error);if(!headers)return 1;let bearer=value("bearer-token-env-var");bearer!==void 0&&(headers.Authorization=`Bearer \${${bearer}}`);let port=value("oauth-callback-port"),oauth={...value("oauth-client-id")===void 0?{}:{clientId:value("oauth-client-id")},...value("oauth-client-secret")===void 0?{}:{clientSecret:value("oauth-client-secret")},...port===void 0?{}:{callbackPort:Number(port)},...value("oauth-client-name")===void 0?{}:{clientName:value("oauth-client-name")},...value("oauth-client-registration")===void 0?{}:{clientRegistration:value("oauth-client-registration")}};config={url,...Object.keys(headers).length>0?{headers}:{},...Object.keys(oauth).length>0?{oauth}:{}}}else{let env=parsePairs("env",lists.get("env"),error);if(!env)return 1;let[executable,...commandArgs]=command;config={command:executable,...commandArgs.length>0?{args:commandArgs}:{},...Object.keys(env).length>0?{env}:{},...value("cwd")===void 0?{}:{cwd:value("cwd")}}}exposure!==void 0&&(config.exposure=exposure);let description=value("description");description!==void 0&&(config.description=description);let validated=validateMcpServerConfig(name,config);if(typeof validated=="string")return error(validated),1;let project=values.has("local"),path=project?projectConfig:join(options.agentDir,"mcp.json"),scope=project?"project":"global",replaced;try{replaced=addMcpServerConfig(path,name,validated)}catch(addError){return error(`Could not update ${path}: ${errorMessage(addError)}`),1}log(`${replaced?"Replaced":"Added"} ${scope} MCP server "${name}" in ${path}.`),project&&new ProjectTrustStore(options.agentDir).get(options.cwd)!==!0&&log(`The project is not trusted, so ${path} is ignored until you start ${APP_NAME} in the project and trust it.`);let mayNeedSignIn="url"in validated&&!Object.keys(validated.headers??{}).some(header=>header.toLowerCase()==="authorization");return log(`Check it with: ${APP_NAME} mcp list${mayNeedSignIn?`. If it requires sign-in: ${APP_NAME} mcp login ${name}`:""}`),0}function remove(args,projectConfig,options,log,error){let parsed=parseOptions(args,{local:"flag"},error);if(!parsed)return 1;let[name,...extra]=parsed.positional;if(!name||extra.length>0)return error(`Usage: ${APP_NAME} mcp remove <server> [-l]
${HELP_HINT}`),1;let project=parsed.values.has("local"),globalConfig=join(options.agentDir,"mcp.json"),path=project?projectConfig:globalConfig,scope=project?"project":"global",removed;try{removed=removeMcpServerConfig(path,name)}catch(removeError){return error(`Could not update ${path}: ${errorMessage(removeError)}`),1}if(removed)return log(`Removed ${scope} MCP server "${name}" from ${path}.`),0;let other=loadMcpConfig({agentDir:options.agentDir,cwd:options.cwd,projectTrusted:!0}).servers.find(server=>server.name===name&&server.scope!==scope);return error(`No ${scope} MCP server named "${name}" in ${path}.${other?` It is defined in ${other.source}${other.scope==="project"?"; use --local":"; omit --local"}.`:""}`),1}async function list(loaded,json,untrustedNote,options,credentials,providerToken,log){let reports=await Promise.all(loaded.servers.map(async entry=>{let authProvider=getAuthProvider(entry),report={name:entry.name,scope:entry.scope??"global",source:entry.source,...entry.override?{override:entry.override}:{},enabled:entry.config.enabled!==!1,exposure:entry.config.exposure??"codemode",transport:describeTransport(entry),...authProvider?{authProvider}:{},state:"disabled",tools:[]};if(!report.enabled)return report;let connection=createConnection(entry,options,credentials,providerToken);try{await connection.getClient()}catch{}report.state=connection.state,report.tools=connection.tools.map(tool=>tool.name);let overrides=connection.tools.flatMap(tool=>{let exposure=getMcpToolExposure(entry.config,tool.name);return exposure===report.exposure?[]:[[tool.name,exposure]]});return overrides.length>0&&(report.toolExposure=Object.fromEntries(overrides)),connection.hasResources&&(report.resources=!0),connection.state!=="connected"&&connection.error&&(report.error=connection.error),await connection.close(),report})),failed=loaded.errors.length>0||reports.some(report=>report.enabled&&report.state!=="connected");if(json)return log(JSON.stringify({servers:reports,errors:loaded.errors,...untrustedNote?{note:untrustedNote}:{}},null,2)),failed?1:0;reports.length===0&&loaded.errors.length===0&&log(`No MCP servers configured. Add them to ${join(options.agentDir,"mcp.json")} or .pi/mcp.json.`);for(let report of reports){let state=report.state==="connected"?`connected, ${report.tools.length} tool${report.tools.length===1?"":"s"}`:report.state==="needs-auth"?"needs sign-in":report.state;if(log(`${report.name}: ${state} (${report.exposure}, ${report.scope})`),log(` ${report.transport}`),report.override&&log(` project override: ${report.override}`),report.state==="needs-auth"&&log(report.authProvider?` sign in with: /login ${report.authProvider} in ${APP_NAME}`:` sign in with: ${APP_NAME} mcp login ${report.name}`),report.tools.length>0){let tools=report.tools.map(tool=>{let exposure=report.toolExposure?.[tool];return exposure?`${tool} [${exposure}]`:tool});log(` tools: ${tools.join(", ")}`)}report.resources&&log(" offers resources"),report.error&&log(` ${report.error.split(`
`).join(`
`)}`)}for(let configError of loaded.errors)log(`config error: ${configError}`);return untrustedNote&&log(untrustedNote),failed?1:0}async function login(entry,connection,url,timeoutMs,options,credentials,log,error){let{name}=entry;try{return await connection.getClient(),log(`Already signed in to MCP server "${name}" (${connection.tools.length} tools).`),0}catch{if(connection.state!=="needs-auth")return error(`MCP server "${name}" failed to connect: ${connection.error??"unknown error"}`),1}let openUrl=options.openUrl??openBrowser,interactive=process.stdin.isTTY===!0&&options.openUrl===void 0;try{await signInMcpServer({serverUrl:url,store:credentials.forServer(name,url),settings:connection.oauthSettings(),challenge:connection.challenge,prompt:{showAuthorizationUrl:authorizationUrl=>{log(`Sign in to MCP server "${name}" in your browser:
${authorizationUrl.href}`),openUrl(authorizationUrl.href)},promptForRedirectUrl:signal=>waitForRedirectUrl(signal,interactive)},signal:AbortSignal.timeout(timeoutMs)})}catch(signInError){return error(signInError instanceof McpSignInCancelledError?`Sign-in to MCP server "${name}" was cancelled or not completed within ${Math.round(timeoutMs/1e3)} seconds.`:`Sign-in to MCP server "${name}" failed: ${errorMessage(signInError)}`),1}connection.challenge=void 0;try{await connection.reconnect()}catch(connectError){return error(`Signed in, but ${errorMessage(connectError)}`),1}return log(`Signed in to MCP server "${name}" (${connection.tools.length} tools).`),0}async function waitForRedirectUrl(signal,interactive){if(!interactive){signal.aborted||await new Promise(resolve=>signal.addEventListener("abort",resolve,{once:!0}));return}try{return await questionTerminal("If the browser cannot reach this machine, paste the URL it was redirected to: ",{signal,output:process.stderr})}catch{return}}export{runMcpCommand};