Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ go.work.sum
# .idea/
# .vscode/

# Generated MCP config
# Generated MCP config (multi-runtime)
.mcp.json
.codex/config.toml
.gemini/settings.json

# Build output (agentfile CLI + agent binaries)
build/
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ bench-report:
bench-all: bench bench-integration bench-report

clean:
rm -rf build/ .agentfile/ .mcp.json
rm -rf build/ .agentfile/ .mcp.json .codex/config.toml .gemini/settings.json
go clean ./...
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ agentfile publish
agentfile install github.com/acme/my-agent
```

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.
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.

**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.

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

```bash
agentfile build
# -> ./build/my-agent binary + .mcp.json for Claude Code
# -> ./build/my-agent binary + MCP config for detected runtimes

# Claude Code auto-discovers the agent — start using it immediately
# Your runtime auto-discovers the agent — start using it immediately
```

### 3. Share it
Expand Down
29 changes: 20 additions & 9 deletions cmd/agentfile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/teabranch/agentfile/pkg/builder"
"github.com/teabranch/agentfile/pkg/definition"
"github.com/teabranch/agentfile/pkg/plugin"
"github.com/teabranch/agentfile/pkg/runtimecfg"
)

// loadSkillFiles reads skill file contents from disk, resolving paths
Expand Down Expand Up @@ -42,6 +43,7 @@ func newBuildCommand() *cobra.Command {
agentName string
pluginFlag bool
parallelism int
runtimeFlag string
)

cmd := &cobra.Command{
Expand All @@ -50,9 +52,10 @@ func newBuildCommand() *cobra.Command {
Long: `Parses the Agentfile, reads each agent's .md file, generates Go source,
and compiles standalone binaries into the output directory.

Also generates/updates .mcp.json with serve-mcp entries for each agent.`,
Also generates/updates MCP config for detected runtimes (Claude Code, Codex, Gemini).
Use --runtime to target a specific runtime or "all" for all supported runtimes.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runBuild(agentfilePath, outputDir, agentName, pluginFlag, parallelism)
return runBuild(agentfilePath, outputDir, agentName, pluginFlag, parallelism, runtimeFlag)
},
}

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

return cmd
}

func runBuild(agentfilePath, outputDir, agentName string, pluginOutput bool, parallelism int) error {
func runBuild(agentfilePath, outputDir, agentName string, pluginOutput bool, parallelism int, runtimeFlag string) error {
if agentfilePath == "" {
agentfilePath = resolveAgentfile()
}
Expand Down Expand Up @@ -121,20 +125,27 @@ func runBuild(agentfilePath, outputDir, agentName string, pluginOutput bool, par
return err
}

// Generate .mcp.json with serve-mcp entries.
// Generate MCP config for target runtimes.
writers, err := runtimecfg.Resolve(runtimeFlag)
if err != nil {
return fmt.Errorf("resolving runtimes: %w", err)
}

absOut, _ := filepath.Abs(outputDir)
entries := make(map[string]MCPServerEntry)
entries := make(map[string]runtimecfg.ServerEntry)
for name := range defs {
entries[name] = MCPServerEntry{
entries[name] = runtimecfg.ServerEntry{
Command: filepath.Join(absOut, name),
Args: []string{"serve-mcp"},
}
}

if err := mergeMCPJSON(".mcp.json", entries); err != nil {
return fmt.Errorf("updating .mcp.json: %w", err)
for _, w := range writers {
if err := w.Merge(w.LocalPath(), entries); err != nil {
return fmt.Errorf("updating %s for %s: %w", w.LocalPath(), w.Runtime(), err)
}
fmt.Printf("Updated %s (%s)\n", w.LocalPath(), w.Runtime())
}
fmt.Println("Updated .mcp.json")

// Generate plugin directories if --plugin flag is set.
if pluginOutput {
Expand Down
81 changes: 53 additions & 28 deletions cmd/agentfile/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ import (
"github.com/teabranch/agentfile/pkg/fsutil"
"github.com/teabranch/agentfile/pkg/github"
"github.com/teabranch/agentfile/pkg/registry"
"github.com/teabranch/agentfile/pkg/runtimecfg"
)

func newInstallCommand() *cobra.Command {
var global bool
var modelOverride string
var runtimeFlag string

cmd := &cobra.Command{
Use: "install <agent-name | github.com/owner/repo[/agent][@version]>",
Short: "Install an agent binary (local or remote)",
Long: `Installs an agent binary and updates the MCP config.
Long: `Installs an agent binary and updates the MCP config for detected runtimes.

Local install (from ./build/):
agentfile install my-agent
Expand All @@ -30,26 +32,32 @@ Remote install (from GitHub Releases):
agentfile install github.com/owner/repo/agent
agentfile install github.com/owner/repo/agent@1.0.0

By default, installs to .agentfile/bin/ (project-local) and updates .mcp.json.
With --global, installs to /usr/local/bin/ and updates ~/.claude/mcp.json.
By default, installs to .agentfile/bin/ (project-local) and updates MCP config.
With --global, installs to /usr/local/bin/ and updates global MCP config.

Override settings at install time:
agentfile install --model gpt-5 github.com/owner/repo/agent`,
agentfile install --model gpt-5 github.com/owner/repo/agent
agentfile install --runtime codex github.com/owner/repo/agent`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
writers, err := runtimecfg.Resolve(runtimeFlag)
if err != nil {
return err
}

var agentName string
if github.IsRemoteRef(args[0]) {
parsed, err := github.ParseRef(args[0])
if err != nil {
return err
}
agentName = parsed.Agent
if err := runRemoteInstall(args[0], global); err != nil {
if err := runRemoteInstall(args[0], global, writers); err != nil {
return err
}
} else {
agentName = args[0]
if err := runLocalInstall(args[0], global); err != nil {
if err := runLocalInstall(args[0], global, writers); err != nil {
return err
}
}
Expand All @@ -67,17 +75,18 @@ Override settings at install time:

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

return cmd
}

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

binDir, mcpPath := installPaths(global)
binDir := installBinDir(global)

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

// Update mcp.json.
// Update MCP configs for target runtimes.
absDst, err := filepath.Abs(dst)
if err != nil {
return fmt.Errorf("resolving absolute path: %w", err)
}
entries := map[string]MCPServerEntry{
entries := map[string]runtimecfg.ServerEntry{
name: {
Command: absDst,
Args: []string{"serve-mcp"},
},
}
if err := mergeMCPJSON(mcpPath, entries); err != nil {
return fmt.Errorf("updating %s: %w", mcpPath, err)
if err := mergeRuntimeConfigs(writers, global, entries); err != nil {
return err
}
fmt.Printf("Updated %s\n", mcpPath)

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

func runRemoteInstall(ref string, global bool) error {
func runRemoteInstall(ref string, global bool, writers []runtimecfg.ConfigWriter) error {
parsed, err := github.ParseRef(ref)
if err != nil {
return err
Expand Down Expand Up @@ -196,7 +204,7 @@ func runRemoteInstall(ref string, global bool) error {
}

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

// Wire MCP.
// Wire MCP for target runtimes.
absDst, err := filepath.Abs(dst)
if err != nil {
return fmt.Errorf("resolving absolute path: %w", err)
}
entries := map[string]MCPServerEntry{
entries := map[string]runtimecfg.ServerEntry{
parsed.Agent: {
Command: absDst,
Args: []string{"serve-mcp"},
},
}
if err := mergeMCPJSON(mcpPath, entries); err != nil {
return fmt.Errorf("updating %s: %w", mcpPath, err)
if err := mergeRuntimeConfigs(writers, global, entries); err != nil {
return err
}
fmt.Printf("Updated %s\n", mcpPath)

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

func installPaths(global bool) (binDir, mcpPath string) {
// installBinDir returns the binary install directory.
// Binary location is agentfile-internal, independent of runtime.
func installBinDir(global bool) string {
if global {
binDir = "/usr/local/bin"
home, _ := os.UserHomeDir()
mcpPath = filepath.Join(home, ".claude", "mcp.json")
} else {
binDir = filepath.Join(".agentfile", "bin")
mcpPath = ".mcp.json"
return "/usr/local/bin"
}
return filepath.Join(".agentfile", "bin")
}

// mergeRuntimeConfigs writes MCP server entries to all target runtime configs.
func mergeRuntimeConfigs(writers []runtimecfg.ConfigWriter, global bool, entries map[string]runtimecfg.ServerEntry) error {
for _, w := range writers {
var cfgPath string
if global {
var err error
cfgPath, err = w.GlobalPath()
if err != nil {
return fmt.Errorf("resolving global path for %s: %w", w.Runtime(), err)
}
} else {
cfgPath = w.LocalPath()
}
if err := w.Merge(cfgPath, entries); err != nil {
return fmt.Errorf("updating %s for %s: %w", cfgPath, w.Runtime(), err)
}
fmt.Printf("Updated %s (%s)\n", cfgPath, w.Runtime())
}
return
return nil
}

func trackInstall(name, source, version, path, scope string) error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/agentfile/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
)

const cliVersion = "0.5.0"
const cliVersion = "0.6.0"

func main() {
root := newRootCommand()
Expand Down
53 changes: 0 additions & 53 deletions cmd/agentfile/mcpgen.go

This file was deleted.

Loading