11import { cmd } from "./cmd"
22import { Client } from "@modelcontextprotocol/sdk/client/index.js"
33import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
4- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
54import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
65import * as prompts from "@clack/prompts"
76import { UI } from "../ui"
@@ -13,6 +12,7 @@ import { Instance } from "../../project/instance"
1312import { Installation } from "../../installation"
1413import path from "path"
1514import { Global } from "../../global"
15+ import { modify , applyEdits } from "jsonc-parser"
1616
1717function getAuthStatusIcon ( status : MCP . AuthStatus ) : string {
1818 switch ( status ) {
@@ -366,133 +366,204 @@ export const McpLogoutCommand = cmd({
366366 } ,
367367} )
368368
369- export const McpAddCommand = cmd ( {
370- command : "add" ,
371- describe : "add an MCP server" ,
372- async handler ( ) {
373- UI . empty ( )
374- prompts . intro ( "Add MCP server" )
375-
376- const name = await prompts . text ( {
377- message : "Enter MCP server name" ,
378- validate : ( x ) => ( x && x . length > 0 ? undefined : "Required" ) ,
379- } )
380- if ( prompts . isCancel ( name ) ) throw new UI . CancelledError ( )
381-
382- const type = await prompts . select ( {
383- message : "Select MCP server type" ,
384- options : [
385- {
386- label : "Local" ,
387- value : "local" ,
388- hint : "Run a local command" ,
389- } ,
390- {
391- label : "Remote" ,
392- value : "remote" ,
393- hint : "Connect to a remote URL" ,
394- } ,
395- ] ,
396- } )
397- if ( prompts . isCancel ( type ) ) throw new UI . CancelledError ( )
369+ async function resolveConfigPath ( baseDir : string , global = false ) {
370+ // Check for existing config files (prefer .jsonc over .json, check .opencode/ subdirectory too)
371+ const candidates = [ path . join ( baseDir , "opencode.json" ) , path . join ( baseDir , "opencode.jsonc" ) ]
398372
399- if ( type === "local" ) {
400- const command = await prompts . text ( {
401- message : "Enter command to run" ,
402- placeholder : "e.g., opencode x @modelcontextprotocol/server-filesystem" ,
403- validate : ( x ) => ( x && x . length > 0 ? undefined : "Required" ) ,
404- } )
405- if ( prompts . isCancel ( command ) ) throw new UI . CancelledError ( )
373+ if ( ! global ) {
374+ candidates . push ( path . join ( baseDir , ".opencode" , "opencode.json" ) , path . join ( baseDir , ".opencode" , "opencode.jsonc" ) )
375+ }
406376
407- prompts . log . info ( `Local MCP server " ${ name } " configured with command: ${ command } ` )
408- prompts . outro ( "MCP server added successfully" )
409- return
377+ for ( const candidate of candidates ) {
378+ if ( await Bun . file ( candidate ) . exists ( ) ) {
379+ return candidate
410380 }
381+ }
411382
412- if ( type === "remote" ) {
413- const url = await prompts . text ( {
414- message : "Enter MCP server URL" ,
415- placeholder : "e.g., https://example.com/mcp" ,
416- validate : ( x ) => {
417- if ( ! x ) return "Required"
418- if ( x . length === 0 ) return "Required"
419- const isValid = URL . canParse ( x )
420- return isValid ? undefined : "Invalid URL"
421- } ,
422- } )
423- if ( prompts . isCancel ( url ) ) throw new UI . CancelledError ( )
383+ // Default to opencode.json if none exist
384+ return candidates [ 0 ]
385+ }
424386
425- const useOAuth = await prompts . confirm ( {
426- message : "Does this server require OAuth authentication?" ,
427- initialValue : false ,
428- } )
429- if ( prompts . isCancel ( useOAuth ) ) throw new UI . CancelledError ( )
387+ async function addMcpToConfig ( name : string , mcpConfig : Config . Mcp , configPath : string ) {
388+ const file = Bun . file ( configPath )
389+
390+ let text = "{}"
391+ if ( await file . exists ( ) ) {
392+ text = await file . text ( )
393+ }
394+
395+ // Use jsonc-parser to modify while preserving comments
396+ const edits = modify ( text , [ "mcp" , name ] , mcpConfig , {
397+ formattingOptions : { tabSize : 2 , insertSpaces : true } ,
398+ } )
399+ const result = applyEdits ( text , edits )
400+
401+ await Bun . write ( configPath , result )
402+
403+ return configPath
404+ }
430405
431- if ( useOAuth ) {
432- const hasClientId = await prompts . confirm ( {
433- message : "Do you have a pre-registered client ID?" ,
434- initialValue : false ,
406+ export const McpAddCommand = cmd ( {
407+ command : "add" ,
408+ describe : "add an MCP server" ,
409+ async handler ( ) {
410+ await Instance . provide ( {
411+ directory : process . cwd ( ) ,
412+ async fn ( ) {
413+ UI . empty ( )
414+ prompts . intro ( "Add MCP server" )
415+
416+ const project = Instance . project
417+
418+ // Resolve config paths eagerly for hints
419+ const [ projectConfigPath , globalConfigPath ] = await Promise . all ( [
420+ resolveConfigPath ( Instance . worktree ) ,
421+ resolveConfigPath ( Global . Path . config , true ) ,
422+ ] )
423+
424+ // Determine scope
425+ let configPath = globalConfigPath
426+ if ( project . vcs === "git" ) {
427+ const scopeResult = await prompts . select ( {
428+ message : "Location" ,
429+ options : [
430+ {
431+ label : "Current project" ,
432+ value : projectConfigPath ,
433+ hint : projectConfigPath ,
434+ } ,
435+ {
436+ label : "Global" ,
437+ value : globalConfigPath ,
438+ hint : globalConfigPath ,
439+ } ,
440+ ] ,
441+ } )
442+ if ( prompts . isCancel ( scopeResult ) ) throw new UI . CancelledError ( )
443+ configPath = scopeResult
444+ }
445+
446+ const name = await prompts . text ( {
447+ message : "Enter MCP server name" ,
448+ validate : ( x ) => ( x && x . length > 0 ? undefined : "Required" ) ,
449+ } )
450+ if ( prompts . isCancel ( name ) ) throw new UI . CancelledError ( )
451+
452+ const type = await prompts . select ( {
453+ message : "Select MCP server type" ,
454+ options : [
455+ {
456+ label : "Local" ,
457+ value : "local" ,
458+ hint : "Run a local command" ,
459+ } ,
460+ {
461+ label : "Remote" ,
462+ value : "remote" ,
463+ hint : "Connect to a remote URL" ,
464+ } ,
465+ ] ,
435466 } )
436- if ( prompts . isCancel ( hasClientId ) ) throw new UI . CancelledError ( )
467+ if ( prompts . isCancel ( type ) ) throw new UI . CancelledError ( )
437468
438- if ( hasClientId ) {
439- const clientId = await prompts . text ( {
440- message : "Enter client ID" ,
469+ if ( type === "local" ) {
470+ const command = await prompts . text ( {
471+ message : "Enter command to run" ,
472+ placeholder : "e.g., opencode x @modelcontextprotocol/server-filesystem" ,
441473 validate : ( x ) => ( x && x . length > 0 ? undefined : "Required" ) ,
442474 } )
443- if ( prompts . isCancel ( clientId ) ) throw new UI . CancelledError ( )
475+ if ( prompts . isCancel ( command ) ) throw new UI . CancelledError ( )
444476
445- const hasSecret = await prompts . confirm ( {
446- message : "Do you have a client secret?" ,
477+ const mcpConfig : Config . Mcp = {
478+ type : "local" ,
479+ command : command . split ( " " ) ,
480+ }
481+
482+ await addMcpToConfig ( name , mcpConfig , configPath )
483+ prompts . log . success ( `MCP server "${ name } " added to ${ configPath } ` )
484+ prompts . outro ( "MCP server added successfully" )
485+ return
486+ }
487+
488+ if ( type === "remote" ) {
489+ const url = await prompts . text ( {
490+ message : "Enter MCP server URL" ,
491+ placeholder : "e.g., https://example.com/mcp" ,
492+ validate : ( x ) => {
493+ if ( ! x ) return "Required"
494+ if ( x . length === 0 ) return "Required"
495+ const isValid = URL . canParse ( x )
496+ return isValid ? undefined : "Invalid URL"
497+ } ,
498+ } )
499+ if ( prompts . isCancel ( url ) ) throw new UI . CancelledError ( )
500+
501+ const useOAuth = await prompts . confirm ( {
502+ message : "Does this server require OAuth authentication?" ,
447503 initialValue : false ,
448504 } )
449- if ( prompts . isCancel ( hasSecret ) ) throw new UI . CancelledError ( )
505+ if ( prompts . isCancel ( useOAuth ) ) throw new UI . CancelledError ( )
450506
451- let clientSecret : string | undefined
452- if ( hasSecret ) {
453- const secret = await prompts . password ( {
454- message : "Enter client secret" ,
507+ let mcpConfig : Config . Mcp
508+
509+ if ( useOAuth ) {
510+ const hasClientId = await prompts . confirm ( {
511+ message : "Do you have a pre-registered client ID?" ,
512+ initialValue : false ,
455513 } )
456- if ( prompts . isCancel ( secret ) ) throw new UI . CancelledError ( )
457- clientSecret = secret
514+ if ( prompts . isCancel ( hasClientId ) ) throw new UI . CancelledError ( )
515+
516+ if ( hasClientId ) {
517+ const clientId = await prompts . text ( {
518+ message : "Enter client ID" ,
519+ validate : ( x ) => ( x && x . length > 0 ? undefined : "Required" ) ,
520+ } )
521+ if ( prompts . isCancel ( clientId ) ) throw new UI . CancelledError ( )
522+
523+ const hasSecret = await prompts . confirm ( {
524+ message : "Do you have a client secret?" ,
525+ initialValue : false ,
526+ } )
527+ if ( prompts . isCancel ( hasSecret ) ) throw new UI . CancelledError ( )
528+
529+ let clientSecret : string | undefined
530+ if ( hasSecret ) {
531+ const secret = await prompts . password ( {
532+ message : "Enter client secret" ,
533+ } )
534+ if ( prompts . isCancel ( secret ) ) throw new UI . CancelledError ( )
535+ clientSecret = secret
536+ }
537+
538+ mcpConfig = {
539+ type : "remote" ,
540+ url,
541+ oauth : {
542+ clientId,
543+ ...( clientSecret && { clientSecret } ) ,
544+ } ,
545+ }
546+ } else {
547+ mcpConfig = {
548+ type : "remote" ,
549+ url,
550+ oauth : { } ,
551+ }
552+ }
553+ } else {
554+ mcpConfig = {
555+ type : "remote" ,
556+ url,
557+ }
458558 }
459559
460- prompts . log . info ( `Remote MCP server "${ name } " configured with OAuth (client ID: ${ clientId } )` )
461- prompts . log . info ( "Add this to your opencode.json:" )
462- prompts . log . info ( `
463- "mcp": {
464- "${ name } ": {
465- "type": "remote",
466- "url": "${ url } ",
467- "oauth": {
468- "clientId": "${ clientId } "${ clientSecret ? `,\n "clientSecret": "${ clientSecret } "` : "" }
469- }
470- }
471- }` )
472- } else {
473- prompts . log . info ( `Remote MCP server "${ name } " configured with OAuth (dynamic registration)` )
474- prompts . log . info ( "Add this to your opencode.json:" )
475- prompts . log . info ( `
476- "mcp": {
477- "${ name } ": {
478- "type": "remote",
479- "url": "${ url } ",
480- "oauth": {}
481- }
482- }` )
560+ await addMcpToConfig ( name , mcpConfig , configPath )
561+ prompts . log . success ( `MCP server "${ name } " added to ${ configPath } ` )
483562 }
484- } else {
485- const client = new Client ( {
486- name : "opencode" ,
487- version : "1.0.0" ,
488- } )
489- const transport = new StreamableHTTPClientTransport ( new URL ( url ) )
490- await client . connect ( transport )
491- prompts . log . info ( `Remote MCP server "${ name } " configured with URL: ${ url } ` )
492- }
493- }
494563
495- prompts . outro ( "MCP server added successfully" )
564+ prompts . outro ( "MCP server added successfully" )
565+ } ,
566+ } )
496567 } ,
497568} )
498569
0 commit comments