Skip to content

Commit 8615ccf

Browse files
authored
Merge pull request #4 from teabranch/feature/multi-runtime-config
Add multi-runtime MCP config support (v0.6.0)
2 parents 7e57ecd + 1ee807d commit 8615ccf

24 files changed

Lines changed: 908 additions & 185 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ go.work.sum
3131
# .idea/
3232
# .vscode/
3333

34-
# Generated MCP config
34+
# Generated MCP config (multi-runtime)
3535
.mcp.json
36+
.codex/config.toml
37+
.gemini/settings.json
3638

3739
# Build output (agentfile CLI + agent binaries)
3840
build/

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ bench-report:
4343
bench-all: bench bench-integration bench-report
4444

4545
clean:
46-
rm -rf build/ .agentfile/ .mcp.json
46+
rm -rf build/ .agentfile/ .mcp.json .codex/config.toml .gemini/settings.json
4747
go clean ./...

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ agentfile publish
4040
agentfile install github.com/acme/my-agent
4141
```
4242

43-
That last command downloads the right binary for your platform, wires it into Claude Code via MCP, and tracks it for future updates. No cloning, no building from source, no editing config files.
43+
That last command downloads the right binary for your platform, wires it into your MCP-compatible runtime (Claude Code, Codex, Gemini CLI — auto-detected), and tracks it for future updates. No cloning, no building from source, no editing config files.
4444

4545
**Claude Code is the brain. The binary is the body** — it provides the instructions, the hands (tools), and the memory. Claude Code loads the agent's prompt, discovers its tools via MCP, and handles all reasoning.
4646

@@ -90,9 +90,9 @@ You are a helpful coding assistant. Use your tools to read and modify files.
9090

9191
```bash
9292
agentfile build
93-
# -> ./build/my-agent binary + .mcp.json for Claude Code
93+
# -> ./build/my-agent binary + MCP config for detected runtimes
9494
95-
# Claude Code auto-discovers the agent — start using it immediately
95+
# Your runtime auto-discovers the agent — start using it immediately
9696
```
9797

9898
### 3. Share it

cmd/agentfile/build.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/teabranch/agentfile/pkg/builder"
1010
"github.com/teabranch/agentfile/pkg/definition"
1111
"github.com/teabranch/agentfile/pkg/plugin"
12+
"github.com/teabranch/agentfile/pkg/runtimecfg"
1213
)
1314

1415
// loadSkillFiles reads skill file contents from disk, resolving paths
@@ -42,6 +43,7 @@ func newBuildCommand() *cobra.Command {
4243
agentName string
4344
pluginFlag bool
4445
parallelism int
46+
runtimeFlag string
4547
)
4648

4749
cmd := &cobra.Command{
@@ -50,9 +52,10 @@ func newBuildCommand() *cobra.Command {
5052
Long: `Parses the Agentfile, reads each agent's .md file, generates Go source,
5153
and compiles standalone binaries into the output directory.
5254
53-
Also generates/updates .mcp.json with serve-mcp entries for each agent.`,
55+
Also generates/updates MCP config for detected runtimes (Claude Code, Codex, Gemini).
56+
Use --runtime to target a specific runtime or "all" for all supported runtimes.`,
5457
RunE: func(cmd *cobra.Command, args []string) error {
55-
return runBuild(agentfilePath, outputDir, agentName, pluginFlag, parallelism)
58+
return runBuild(agentfilePath, outputDir, agentName, pluginFlag, parallelism, runtimeFlag)
5659
},
5760
}
5861

@@ -61,11 +64,12 @@ Also generates/updates .mcp.json with serve-mcp entries for each agent.`,
6164
cmd.Flags().StringVar(&agentName, "agent", "", "Build a single agent by name")
6265
cmd.Flags().BoolVar(&pluginFlag, "plugin", false, "Also generate a Claude Code plugin directory")
6366
cmd.Flags().IntVar(&parallelism, "parallelism", 0, "Max concurrent agent builds (0 = sequential)")
67+
cmd.Flags().StringVar(&runtimeFlag, "runtime", "auto", "Target runtime: auto, all, claude-code, codex, gemini")
6468

6569
return cmd
6670
}
6771

68-
func runBuild(agentfilePath, outputDir, agentName string, pluginOutput bool, parallelism int) error {
72+
func runBuild(agentfilePath, outputDir, agentName string, pluginOutput bool, parallelism int, runtimeFlag string) error {
6973
if agentfilePath == "" {
7074
agentfilePath = resolveAgentfile()
7175
}
@@ -121,20 +125,27 @@ func runBuild(agentfilePath, outputDir, agentName string, pluginOutput bool, par
121125
return err
122126
}
123127

124-
// Generate .mcp.json with serve-mcp entries.
128+
// Generate MCP config for target runtimes.
129+
writers, err := runtimecfg.Resolve(runtimeFlag)
130+
if err != nil {
131+
return fmt.Errorf("resolving runtimes: %w", err)
132+
}
133+
125134
absOut, _ := filepath.Abs(outputDir)
126-
entries := make(map[string]MCPServerEntry)
135+
entries := make(map[string]runtimecfg.ServerEntry)
127136
for name := range defs {
128-
entries[name] = MCPServerEntry{
137+
entries[name] = runtimecfg.ServerEntry{
129138
Command: filepath.Join(absOut, name),
130139
Args: []string{"serve-mcp"},
131140
}
132141
}
133142

134-
if err := mergeMCPJSON(".mcp.json", entries); err != nil {
135-
return fmt.Errorf("updating .mcp.json: %w", err)
143+
for _, w := range writers {
144+
if err := w.Merge(w.LocalPath(), entries); err != nil {
145+
return fmt.Errorf("updating %s for %s: %w", w.LocalPath(), w.Runtime(), err)
146+
}
147+
fmt.Printf("Updated %s (%s)\n", w.LocalPath(), w.Runtime())
136148
}
137-
fmt.Println("Updated .mcp.json")
138149

139150
// Generate plugin directories if --plugin flag is set.
140151
if pluginOutput {

cmd/agentfile/install.go

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ import (
1212
"github.com/teabranch/agentfile/pkg/fsutil"
1313
"github.com/teabranch/agentfile/pkg/github"
1414
"github.com/teabranch/agentfile/pkg/registry"
15+
"github.com/teabranch/agentfile/pkg/runtimecfg"
1516
)
1617

1718
func newInstallCommand() *cobra.Command {
1819
var global bool
1920
var modelOverride string
21+
var runtimeFlag string
2022

2123
cmd := &cobra.Command{
2224
Use: "install <agent-name | github.com/owner/repo[/agent][@version]>",
2325
Short: "Install an agent binary (local or remote)",
24-
Long: `Installs an agent binary and updates the MCP config.
26+
Long: `Installs an agent binary and updates the MCP config for detected runtimes.
2527
2628
Local install (from ./build/):
2729
agentfile install my-agent
@@ -30,26 +32,32 @@ Remote install (from GitHub Releases):
3032
agentfile install github.com/owner/repo/agent
3133
agentfile install github.com/owner/repo/agent@1.0.0
3234
33-
By default, installs to .agentfile/bin/ (project-local) and updates .mcp.json.
34-
With --global, installs to /usr/local/bin/ and updates ~/.claude/mcp.json.
35+
By default, installs to .agentfile/bin/ (project-local) and updates MCP config.
36+
With --global, installs to /usr/local/bin/ and updates global MCP config.
3537
3638
Override settings at install time:
37-
agentfile install --model gpt-5 github.com/owner/repo/agent`,
39+
agentfile install --model gpt-5 github.com/owner/repo/agent
40+
agentfile install --runtime codex github.com/owner/repo/agent`,
3841
Args: cobra.ExactArgs(1),
3942
RunE: func(cmd *cobra.Command, args []string) error {
43+
writers, err := runtimecfg.Resolve(runtimeFlag)
44+
if err != nil {
45+
return err
46+
}
47+
4048
var agentName string
4149
if github.IsRemoteRef(args[0]) {
4250
parsed, err := github.ParseRef(args[0])
4351
if err != nil {
4452
return err
4553
}
4654
agentName = parsed.Agent
47-
if err := runRemoteInstall(args[0], global); err != nil {
55+
if err := runRemoteInstall(args[0], global, writers); err != nil {
4856
return err
4957
}
5058
} else {
5159
agentName = args[0]
52-
if err := runLocalInstall(args[0], global); err != nil {
60+
if err := runLocalInstall(args[0], global, writers); err != nil {
5361
return err
5462
}
5563
}
@@ -67,17 +75,18 @@ Override settings at install time:
6775

6876
cmd.Flags().BoolVarP(&global, "global", "g", false, "Install globally to /usr/local/bin")
6977
cmd.Flags().StringVar(&modelOverride, "model", "", "Override the agent's model in ~/.agentfile/<name>/config.yaml")
78+
cmd.Flags().StringVar(&runtimeFlag, "runtime", "auto", "Target runtime: auto, all, claude-code, codex, gemini")
7079

7180
return cmd
7281
}
7382

74-
func runLocalInstall(name string, global bool) error {
83+
func runLocalInstall(name string, global bool, writers []runtimecfg.ConfigWriter) error {
7584
src := filepath.Join("build", name)
7685
if _, err := os.Stat(src); err != nil {
7786
return fmt.Errorf("binary not found: %s (run 'agentfile build' first)", src)
7887
}
7988

80-
binDir, mcpPath := installPaths(global)
89+
binDir := installBinDir(global)
8190

8291
if err := os.MkdirAll(binDir, 0o755); err != nil {
8392
return fmt.Errorf("creating bin dir: %w", err)
@@ -92,21 +101,20 @@ func runLocalInstall(name string, global bool) error {
92101
}
93102
fmt.Printf("Installed %s → %s\n", name, dst)
94103

95-
// Update mcp.json.
104+
// Update MCP configs for target runtimes.
96105
absDst, err := filepath.Abs(dst)
97106
if err != nil {
98107
return fmt.Errorf("resolving absolute path: %w", err)
99108
}
100-
entries := map[string]MCPServerEntry{
109+
entries := map[string]runtimecfg.ServerEntry{
101110
name: {
102111
Command: absDst,
103112
Args: []string{"serve-mcp"},
104113
},
105114
}
106-
if err := mergeMCPJSON(mcpPath, entries); err != nil {
107-
return fmt.Errorf("updating %s: %w", mcpPath, err)
115+
if err := mergeRuntimeConfigs(writers, global, entries); err != nil {
116+
return err
108117
}
109-
fmt.Printf("Updated %s\n", mcpPath)
110118

111119
// Track in registry.
112120
version := ""
@@ -120,7 +128,7 @@ func runLocalInstall(name string, global bool) error {
120128
return trackInstall(name, "local", version, absDst, scope)
121129
}
122130

123-
func runRemoteInstall(ref string, global bool) error {
131+
func runRemoteInstall(ref string, global bool, writers []runtimecfg.ConfigWriter) error {
124132
parsed, err := github.ParseRef(ref)
125133
if err != nil {
126134
return err
@@ -196,7 +204,7 @@ func runRemoteInstall(ref string, global bool) error {
196204
}
197205

198206
// Move to install location.
199-
binDir, mcpPath := installPaths(global)
207+
binDir := installBinDir(global)
200208
if err := os.MkdirAll(binDir, 0o755); err != nil {
201209
return fmt.Errorf("creating bin dir: %w", err)
202210
}
@@ -210,21 +218,20 @@ func runRemoteInstall(ref string, global bool) error {
210218
}
211219
fmt.Printf("Installed %s → %s\n", parsed.Agent, dst)
212220

213-
// Wire MCP.
221+
// Wire MCP for target runtimes.
214222
absDst, err := filepath.Abs(dst)
215223
if err != nil {
216224
return fmt.Errorf("resolving absolute path: %w", err)
217225
}
218-
entries := map[string]MCPServerEntry{
226+
entries := map[string]runtimecfg.ServerEntry{
219227
parsed.Agent: {
220228
Command: absDst,
221229
Args: []string{"serve-mcp"},
222230
},
223231
}
224-
if err := mergeMCPJSON(mcpPath, entries); err != nil {
225-
return fmt.Errorf("updating %s: %w", mcpPath, err)
232+
if err := mergeRuntimeConfigs(writers, global, entries); err != nil {
233+
return err
226234
}
227-
fmt.Printf("Updated %s\n", mcpPath)
228235

229236
// Track in registry.
230237
source := fmt.Sprintf("github.com/%s/%s/%s", parsed.Owner, parsed.Repo, parsed.Agent)
@@ -235,16 +242,34 @@ func runRemoteInstall(ref string, global bool) error {
235242
return trackInstall(parsed.Agent, source, manifest.Version, absDst, scope)
236243
}
237244

238-
func installPaths(global bool) (binDir, mcpPath string) {
245+
// installBinDir returns the binary install directory.
246+
// Binary location is agentfile-internal, independent of runtime.
247+
func installBinDir(global bool) string {
239248
if global {
240-
binDir = "/usr/local/bin"
241-
home, _ := os.UserHomeDir()
242-
mcpPath = filepath.Join(home, ".claude", "mcp.json")
243-
} else {
244-
binDir = filepath.Join(".agentfile", "bin")
245-
mcpPath = ".mcp.json"
249+
return "/usr/local/bin"
250+
}
251+
return filepath.Join(".agentfile", "bin")
252+
}
253+
254+
// mergeRuntimeConfigs writes MCP server entries to all target runtime configs.
255+
func mergeRuntimeConfigs(writers []runtimecfg.ConfigWriter, global bool, entries map[string]runtimecfg.ServerEntry) error {
256+
for _, w := range writers {
257+
var cfgPath string
258+
if global {
259+
var err error
260+
cfgPath, err = w.GlobalPath()
261+
if err != nil {
262+
return fmt.Errorf("resolving global path for %s: %w", w.Runtime(), err)
263+
}
264+
} else {
265+
cfgPath = w.LocalPath()
266+
}
267+
if err := w.Merge(cfgPath, entries); err != nil {
268+
return fmt.Errorf("updating %s for %s: %w", cfgPath, w.Runtime(), err)
269+
}
270+
fmt.Printf("Updated %s (%s)\n", cfgPath, w.Runtime())
246271
}
247-
return
272+
return nil
248273
}
249274

250275
func trackInstall(name, source, version, path, scope string) error {

cmd/agentfile/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/spf13/cobra"
1010
)
1111

12-
const cliVersion = "0.5.0"
12+
const cliVersion = "0.6.0"
1313

1414
func main() {
1515
root := newRootCommand()

cmd/agentfile/mcpgen.go

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)