streamable HTTP

This commit is contained in:
Ed Zynda
2025-06-19 16:16:19 +03:00
parent 33df79adb6
commit 1f407bf7fb
11 changed files with 565 additions and 451 deletions
+34
View File
@@ -151,6 +151,40 @@ Each SSE entry requires:
- `url`: The URL where the MCP server is accessible.
- `headers`: (Optional) Array of headers that will be attached to the requests
### Streamable HTTP
For Streamable HTTP transport, use the following configuration:
```json
{
"mcpServers": {
"websearch": {
"transport": "streamable",
"url": "https://api.example.com/mcp",
"headers": [
"Authorization: Bearer your-api-token",
"Content-Type: application/json"
]
}
}
}
```
Each Streamable HTTP entry requires:
- `transport`: Must be set to `"streamable"`
- `url`: The URL where the MCP server is accessible
- `headers`: (Optional) Array of headers that will be attached to the requests
### Transport Types
MCPHost supports three transport types:
- **`stdio`** (default): Launches a local process and communicates via stdin/stdout
- **`sse`**: Connects to a server using Server-Sent Events
- **`streamable`**: Connects to a server using Streamable HTTP protocol
If no `transport` field is specified, MCPHost will automatically detect the transport type:
- If `command` is present → `stdio`
- If `url` is present → `sse`
### System Prompt
You can specify a custom system prompt using the `--system-prompt` flag. You can either:
+1 -1
View File
@@ -84,7 +84,7 @@ func initConfig() {
if configFile != "" {
// Use config file from the flag
viper.SetConfigFile(configFile)
// Try to read the specified config file
if err := viper.ReadInConfig(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading config file '%s': %v\n", configFile, err)
+1 -1
View File
@@ -13,7 +13,7 @@ require (
github.com/cloudwego/eino-ext/components/model/ollama v0.0.0-20250609074000-b7f307dffa18
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250609074000-b7f307dffa18
github.com/getkin/kin-openapi v0.118.0
github.com/mark3labs/mcp-go v0.31.0
github.com/mark3labs/mcp-go v0.32.0
github.com/ollama/ollama v0.5.12
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.20.1
+2 -2
View File
@@ -196,8 +196,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+44 -1
View File
@@ -9,6 +9,7 @@ import (
// MCPServerConfig represents configuration for an MCP server
type MCPServerConfig struct {
Transport string `json:"transport,omitempty"`
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Env map[string]any `json:"env,omitempty"`
@@ -38,12 +39,41 @@ type Config struct {
StopSequences []string `json:"stop-sequences,omitempty" yaml:"stop-sequences,omitempty"`
}
// GetTransportType returns the transport type for the server config
func (s *MCPServerConfig) GetTransportType() string {
if s.Transport != "" {
return s.Transport
}
// Backward compatibility: infer transport type
if s.Command != "" {
return "stdio"
}
if s.URL != "" {
return "sse"
}
return "stdio" // default
}
// Validate validates the configuration
func (c *Config) Validate() error {
for serverName, serverConfig := range c.MCPServers {
if len(serverConfig.AllowedTools) > 0 && len(serverConfig.ExcludedTools) > 0 {
return fmt.Errorf("server %s: allowedTools and excludedTools are mutually exclusive", serverName)
}
transport := serverConfig.GetTransportType()
switch transport {
case "stdio":
if serverConfig.Command == "" {
return fmt.Errorf("server %s: command is required for stdio transport", serverName)
}
case "sse", "streamable":
if serverConfig.URL == "" {
return fmt.Errorf("server %s: url is required for %s transport", serverName, transport)
}
default:
return fmt.Errorf("server %s: unsupported transport type '%s'. Supported types: stdio, sse, streamable", serverName, transport)
}
}
return nil
}
@@ -110,14 +140,27 @@ func createDefaultConfig(homeDir string) error {
# MCP Servers configuration
# Add your MCP servers here
# Example:
# Examples for different transport types:
# mcpServers:
# # STDIO transport (default) - launches local processes
# filesystem:
# command: npx
# args: ["@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"]
# sqlite:
# command: uvx
# args: ["mcp-server-sqlite", "--db-path", "/tmp/example.db"]
#
# # SSE transport - connects to remote servers via Server-Sent Events
# remote-sse:
# transport: sse
# url: "https://api.example.com/sse"
# headers: ["Authorization: Bearer your-token"]
#
# # Streamable HTTP transport - connects via Streamable HTTP protocol
# websearch:
# transport: streamable
# url: "https://api.example.com/mcp"
# headers: ["Authorization: Bearer your-api-token"]
mcpServers:
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
package tokens
package tokens
+1 -1
View File
@@ -4,4 +4,4 @@ package tokens
func EstimateTokens(text string) int {
// Rough approximation: ~4 characters per token for most models
return len(text) / 4
}
}
+1 -1
View File
@@ -8,4 +8,4 @@ func InitializeTokenCounters() {
// InitializeTokenCountersWithKeys registers token counters with provided API keys
func InitializeTokenCountersWithKeys() {
// Future provider-specific counters can be registered here
}
}
+43 -4
View File
@@ -4,12 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/bytedance/sonic"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/getkin/kin-openapi/openapi3"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcphost/internal/config"
)
@@ -192,7 +194,10 @@ func (m *MCPToolManager) shouldExcludeTool(toolName string, serverConfig config.
}
func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string, serverConfig config.MCPServerConfig) (client.MCPClient, error) {
if serverConfig.Command != "" {
transportType := serverConfig.GetTransportType()
switch transportType {
case "stdio":
// STDIO client
env := make([]string, 0, len(serverConfig.Env))
for k, v := range serverConfig.Env {
@@ -200,7 +205,8 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
}
return client.NewStdioMCPClient(serverConfig.Command, env, serverConfig.Args...)
} else if serverConfig.URL != "" {
case "sse":
// SSE client
sseClient, err := client.NewSSEMCPClient(serverConfig.URL)
if err != nil {
@@ -213,9 +219,42 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
}
return sseClient, nil
}
return nil, fmt.Errorf("invalid server configuration for %s: must specify either command or url", serverName)
case "streamable":
// Streamable HTTP client
var options []transport.StreamableHTTPCOption
// Add headers if specified
if len(serverConfig.Headers) > 0 {
headers := make(map[string]string)
for _, header := range serverConfig.Headers {
parts := strings.SplitN(header, ":", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
headers[key] = value
}
}
if len(headers) > 0 {
options = append(options, transport.WithHTTPHeaders(headers))
}
}
streamableClient, err := client.NewStreamableHttpClient(serverConfig.URL, options...)
if err != nil {
return nil, err
}
// Start the streamable HTTP client
if err := streamableClient.Start(ctx); err != nil {
return nil, fmt.Errorf("failed to start streamable HTTP client: %v", err)
}
return streamableClient, nil
default:
return nil, fmt.Errorf("unsupported transport type '%s' for server %s", transportType, serverName)
}
}
func (m *MCPToolManager) initializeClient(ctx context.Context, client client.MCPClient) error {
+3 -5
View File
@@ -322,8 +322,6 @@ func (c *CLI) UpdateUsage(inputText, outputText string) {
}
}
// UpdateUsageFromResponse updates the usage tracker using token usage from response metadata
func (c *CLI) UpdateUsageFromResponse(response *schema.Message, inputText string) {
if c.usageTracker == nil {
@@ -333,15 +331,15 @@ func (c *CLI) UpdateUsageFromResponse(response *schema.Message, inputText string
// Try to extract token usage from response metadata
if response.ResponseMeta != nil && response.ResponseMeta.Usage != nil {
usage := response.ResponseMeta.Usage
// Use actual token counts from the response
inputTokens := int(usage.PromptTokens)
outputTokens := int(usage.CompletionTokens)
// Handle cache tokens if available (some providers support this)
cacheReadTokens := 0
cacheWriteTokens := 0
c.usageTracker.UpdateUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens)
} else {
// Fallback to estimation if no metadata is available