Skip to content

Commit 5959f15

Browse files
committed
more updates
1 parent 6c4bcce commit 5959f15

File tree

4 files changed

+262
-53
lines changed

4 files changed

+262
-53
lines changed

README.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,15 @@ go install github.com/mark3labs/mcphost@latest
8080
## Configuration ⚙️
8181

8282
### MCP-server
83-
MCPHost will automatically create a configuration file at `~/.mcp.json` if it doesn't exist. You can also specify a custom location using the `--config` flag.
83+
MCPHost will automatically create a configuration file in your home directory if it doesn't exist. It looks for config files in this order:
84+
- `.mcphost.yml` or `.mcphost.json` (preferred)
85+
- `.mcp.yml` or `.mcp.json` (backwards compatibility)
86+
87+
**Config file locations by OS:**
88+
- **Linux/macOS**: `~/.mcphost.yml`, `~/.mcphost.json`, `~/.mcp.yml`, `~/.mcp.json`
89+
- **Windows**: `%USERPROFILE%\.mcphost.yml`, `%USERPROFILE%\.mcphost.json`, `%USERPROFILE%\.mcp.yml`, `%USERPROFILE%\.mcp.json`
90+
91+
You can also specify a custom location using the `--config` flag.
8492

8593
#### STDIO
8694
The configuration for an STDIO MCP-server should be defined as the following:
@@ -268,18 +276,48 @@ mcphost -p "Generate a random UUID" --quiet | tr '[:lower:]' '[:upper:]'
268276
### Flags
269277
- `--anthropic-url string`: Base URL for Anthropic API (defaults to api.anthropic.com)
270278
- `--anthropic-api-key string`: Anthropic API key (can also be set via ANTHROPIC_API_KEY environment variable)
271-
- `--config string`: Config file location (default is $HOME/.mcp.json)
279+
- `--config string`: Config file location (default is $HOME/.mcphost.yml)
272280
- `--system-prompt string`: system-prompt file location
273281
- `--debug`: Enable debug logging
274-
- `--message-window int`: Number of messages to keep in context (default: 10)
275-
- `-m, --model string`: Model to use (format: provider:model) (default "anthropic:claude-3-5-sonnet-latest")
282+
- `--max-steps int`: Maximum number of agent steps (0 for unlimited, default: 0)
283+
- `--message-window int`: Number of messages to keep in context (default: 40)
284+
- `-m, --model string`: Model to use (format: provider:model) (default "anthropic:claude-sonnet-4-20250514")
276285
- `--openai-url string`: Base URL for OpenAI API (defaults to api.openai.com)
277286
- `--openai-api-key string`: OpenAI API key (can also be set via OPENAI_API_KEY environment variable)
278287
- `--google-api-key string`: Google API key (can also be set via GOOGLE_API_KEY environment variable)
279288
- `-p, --prompt string`: **Run in non-interactive mode with the given prompt**
280289
- `--quiet`: **Suppress all output except the AI response (only works with --prompt)**
281290
- `--script`: **Run in script mode (parse YAML frontmatter and prompt from file)**
282291

292+
### Configuration File Support
293+
294+
All command-line flags can be configured via the config file. MCPHost will look for configuration in this order:
295+
1. `~/.mcphost.yml` or `~/.mcphost.json` (preferred)
296+
2. `~/.mcp.yml` or `~/.mcp.json` (backwards compatibility)
297+
298+
Example config file (`~/.mcphost.yml`):
299+
```yaml
300+
# MCP Servers
301+
mcpServers:
302+
filesystem:
303+
command: npx
304+
args: ["@modelcontextprotocol/server-filesystem", "/path/to/files"]
305+
306+
# Application settings
307+
model: "anthropic:claude-sonnet-4-20250514"
308+
max-steps: 20
309+
message-window: 40
310+
debug: false
311+
system-prompt: "/path/to/system-prompt.json"
312+
313+
# API keys (can also use environment variables)
314+
anthropic-api-key: "your-key-here"
315+
openai-api-key: "your-key-here"
316+
google-api-key: "your-key-here"
317+
```
318+
319+
**Note**: Command-line flags take precedence over config file values.
320+
283321

284322
### Interactive Commands
285323

cmd/root.go

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/mark3labs/mcphost/internal/models"
1616
"github.com/mark3labs/mcphost/internal/ui"
1717
"github.com/spf13/cobra"
18+
"github.com/spf13/viper"
1819
"gopkg.in/yaml.v3"
1920
)
2021

@@ -32,6 +33,7 @@ var (
3233
promptFlag string
3334
quietFlag bool
3435
scriptFlag bool
36+
maxSteps int
3537
scriptMCPConfig *config.Config // Used to override config in script mode
3638
)
3739

@@ -79,7 +81,7 @@ func init() {
7981
rootCmd.PersistentFlags().
8082
StringVar(&systemPromptFile, "system-prompt", "", "system prompt json file")
8183
rootCmd.PersistentFlags().
82-
IntVar(&messageWindow, "message-window", 10, "number of messages to keep in context")
84+
IntVar(&messageWindow, "message-window", 40, "number of messages to keep in context")
8385
rootCmd.PersistentFlags().
8486
StringVarP(&modelFlag, "model", "m", "anthropic:claude-sonnet-4-20250514",
8587
"model to use (format: provider:model)")
@@ -91,13 +93,27 @@ func init() {
9193
BoolVar(&quietFlag, "quiet", false, "suppress all output (only works with --prompt)")
9294
rootCmd.PersistentFlags().
9395
BoolVar(&scriptFlag, "script", false, "run in script mode (parse YAML frontmatter and prompt from file)")
96+
rootCmd.PersistentFlags().
97+
IntVar(&maxSteps, "max-steps", 0, "maximum number of agent steps (0 for unlimited)")
9498

9599
flags := rootCmd.PersistentFlags()
96100
flags.StringVar(&openaiBaseURL, "openai-url", "", "base URL for OpenAI API")
97101
flags.StringVar(&anthropicBaseURL, "anthropic-url", "", "base URL for Anthropic API")
98102
flags.StringVar(&openaiAPIKey, "openai-api-key", "", "OpenAI API key")
99103
flags.StringVar(&anthropicAPIKey, "anthropic-api-key", "", "Anthropic API key")
100104
flags.StringVar(&googleAPIKey, "google-api-key", "", "Google (Gemini) API key")
105+
106+
// Bind flags to viper for config file support
107+
viper.BindPFlag("system-prompt", rootCmd.PersistentFlags().Lookup("system-prompt"))
108+
viper.BindPFlag("message-window", rootCmd.PersistentFlags().Lookup("message-window"))
109+
viper.BindPFlag("model", rootCmd.PersistentFlags().Lookup("model"))
110+
viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug"))
111+
viper.BindPFlag("max-steps", rootCmd.PersistentFlags().Lookup("max-steps"))
112+
viper.BindPFlag("openai-url", rootCmd.PersistentFlags().Lookup("openai-url"))
113+
viper.BindPFlag("anthropic-url", rootCmd.PersistentFlags().Lookup("anthropic-url"))
114+
viper.BindPFlag("openai-api-key", rootCmd.PersistentFlags().Lookup("openai-api-key"))
115+
viper.BindPFlag("anthropic-api-key", rootCmd.PersistentFlags().Lookup("anthropic-api-key"))
116+
viper.BindPFlag("google-api-key", rootCmd.PersistentFlags().Lookup("google-api-key"))
101117
}
102118

103119
func runMCPHost(ctx context.Context) error {
@@ -135,6 +151,66 @@ func runNormalMode(ctx context.Context) error {
135151
}
136152
}
137153

154+
// Set up viper to read from the same config file for flag values
155+
if configFile == "" {
156+
// Use default config file locations
157+
homeDir, err := os.UserHomeDir()
158+
if err == nil {
159+
viper.SetConfigName(".mcphost")
160+
viper.AddConfigPath(homeDir)
161+
viper.SetConfigType("yaml")
162+
if err := viper.ReadInConfig(); err != nil {
163+
// Try .mcphost.json
164+
viper.SetConfigType("json")
165+
if err := viper.ReadInConfig(); err != nil {
166+
// Try legacy .mcp files
167+
viper.SetConfigName(".mcp")
168+
viper.SetConfigType("yaml")
169+
if err := viper.ReadInConfig(); err != nil {
170+
viper.SetConfigType("json")
171+
viper.ReadInConfig() // Ignore error if no config found
172+
}
173+
}
174+
}
175+
}
176+
} else {
177+
// Use specified config file
178+
viper.SetConfigFile(configFile)
179+
viper.ReadInConfig() // Ignore error if file doesn't exist
180+
}
181+
182+
// Override flag values with config file values (using viper's bound values)
183+
if viper.GetString("system-prompt") != "" {
184+
systemPromptFile = viper.GetString("system-prompt")
185+
}
186+
if viper.GetInt("message-window") != 0 {
187+
messageWindow = viper.GetInt("message-window")
188+
}
189+
if viper.GetString("model") != "" {
190+
modelFlag = viper.GetString("model")
191+
}
192+
if viper.GetBool("debug") {
193+
debugMode = viper.GetBool("debug")
194+
}
195+
if viper.GetInt("max-steps") != 0 {
196+
maxSteps = viper.GetInt("max-steps")
197+
}
198+
if viper.GetString("openai-url") != "" {
199+
openaiBaseURL = viper.GetString("openai-url")
200+
}
201+
if viper.GetString("anthropic-url") != "" {
202+
anthropicBaseURL = viper.GetString("anthropic-url")
203+
}
204+
if viper.GetString("openai-api-key") != "" {
205+
openaiAPIKey = viper.GetString("openai-api-key")
206+
}
207+
if viper.GetString("anthropic-api-key") != "" {
208+
anthropicAPIKey = viper.GetString("anthropic-api-key")
209+
}
210+
if viper.GetString("google-api-key") != "" {
211+
googleAPIKey = viper.GetString("google-api-key")
212+
}
213+
138214
systemPrompt, err := config.LoadSystemPrompt(systemPromptFile)
139215
if err != nil {
140216
return fmt.Errorf("failed to load system prompt: %v", err)
@@ -152,11 +228,16 @@ func runNormalMode(ctx context.Context) error {
152228
}
153229

154230
// Create agent configuration
231+
agentMaxSteps := maxSteps
232+
if agentMaxSteps == 0 {
233+
agentMaxSteps = 1000 // Set a high limit for "unlimited"
234+
}
235+
155236
agentConfig := &agent.AgentConfig{
156237
ModelConfig: modelConfig,
157238
MCPConfig: mcpConfig,
158239
SystemPrompt: systemPrompt,
159-
MaxSteps: 20,
240+
MaxSteps: agentMaxSteps,
160241
MessageWindow: messageWindow,
161242
}
162243

internal/agent/agent.go

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,27 @@ func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) {
144144
Tools: toolManager.GetTools(),
145145
}
146146

147-
if toolInfos, err = genToolInfos(ctx, toolsConfig); err != nil {
148-
return nil, err
147+
// Only set up tools if we have any
148+
hasTools := len(toolsConfig.Tools) > 0
149+
150+
if hasTools {
151+
if toolInfos, err = genToolInfos(ctx, toolsConfig); err != nil {
152+
return nil, err
153+
}
154+
155+
if toolsNode, err = compose.NewToolNode(ctx, &toolsConfig); err != nil {
156+
return nil, err
157+
}
149158
}
150159

151160
chatModel, err := agent.ChatModelWithTools(nil, model, toolInfos)
152161
if err != nil {
153-
return nil, err
154-
}
155-
156-
if toolsNode, err = compose.NewToolNode(ctx, &toolsConfig); err != nil {
157-
return nil, err
162+
// If binding tools fails and we have no tools, just use the model directly
163+
if !hasTools {
164+
chatModel = model
165+
} else {
166+
return nil, err
167+
}
158168
}
159169

160170
maxSteps := config.MaxSteps
@@ -199,37 +209,45 @@ func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) {
199209
return nil, err
200210
}
201211

202-
toolsNodePreHandle := func(ctx context.Context, input *schema.Message, state *state) (*schema.Message, error) {
203-
if input == nil {
204-
return state.Messages[len(state.Messages)-1], nil // used for rerun interrupt resume
212+
// Only add tools node and related logic if we have tools
213+
if hasTools {
214+
toolsNodePreHandle := func(ctx context.Context, input *schema.Message, state *state) (*schema.Message, error) {
215+
if input == nil {
216+
return state.Messages[len(state.Messages)-1], nil // used for rerun interrupt resume
217+
}
218+
state.Messages = append(state.Messages, input)
219+
state.ReturnDirectlyToolCallID = getReturnDirectlyToolCallID(input, config.ToolReturnDirectly)
220+
return input, nil
221+
}
222+
if err = graph.AddToolsNode(nodeKeyTools, toolsNode, compose.WithStatePreHandler(toolsNodePreHandle), compose.WithNodeName(ToolsNodeName)); err != nil {
223+
return nil, err
205224
}
206-
state.Messages = append(state.Messages, input)
207-
state.ReturnDirectlyToolCallID = getReturnDirectlyToolCallID(input, config.ToolReturnDirectly)
208-
return input, nil
209-
}
210-
if err = graph.AddToolsNode(nodeKeyTools, toolsNode, compose.WithStatePreHandler(toolsNodePreHandle), compose.WithNodeName(ToolsNodeName)); err != nil {
211-
return nil, err
212-
}
213225

214-
modelPostBranchCondition := func(_ context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) {
215-
if isToolCall, err := toolCallChecker(ctx, sr); err != nil {
216-
return "", err
217-
} else if isToolCall {
218-
return nodeKeyTools, nil
226+
modelPostBranchCondition := func(_ context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) {
227+
if isToolCall, err := toolCallChecker(ctx, sr); err != nil {
228+
return "", err
229+
} else if isToolCall {
230+
return nodeKeyTools, nil
231+
}
232+
return compose.END, nil
219233
}
220-
return compose.END, nil
221-
}
222234

223-
if err = graph.AddBranch(nodeKeyModel, compose.NewStreamGraphBranch(modelPostBranchCondition, map[string]bool{nodeKeyTools: true, compose.END: true})); err != nil {
224-
return nil, err
225-
}
235+
if err = graph.AddBranch(nodeKeyModel, compose.NewStreamGraphBranch(modelPostBranchCondition, map[string]bool{nodeKeyTools: true, compose.END: true})); err != nil {
236+
return nil, err
237+
}
226238

227-
if len(config.ToolReturnDirectly) > 0 {
228-
if err = buildReturnDirectly(graph); err != nil {
239+
if len(config.ToolReturnDirectly) > 0 {
240+
if err = buildReturnDirectly(graph); err != nil {
241+
return nil, err
242+
}
243+
} else if err = graph.AddEdge(nodeKeyTools, nodeKeyModel); err != nil {
244+
return nil, err
245+
}
246+
} else {
247+
// No tools, so model goes directly to END
248+
if err = graph.AddEdge(nodeKeyModel, compose.END); err != nil {
229249
return nil, err
230250
}
231-
} else if err = graph.AddEdge(nodeKeyTools, nodeKeyModel); err != nil {
232-
return nil, err
233251
}
234252

235253
compileOpts := []compose.GraphCompileOption{compose.WithMaxRunSteps(maxSteps), compose.WithNodeTriggerMode(compose.AnyPredecessor), compose.WithGraphName(GraphName)}

0 commit comments

Comments
 (0)