Skip to content

Commit df29dc1

Browse files
committed
fix frontmatter parsing
1 parent a87c772 commit df29dc1

File tree

3 files changed

+161
-9
lines changed

3 files changed

+161
-9
lines changed

cmd/script.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"github.com/mark3labs/mcphost/internal/ui"
1717
"github.com/spf13/cobra"
1818
"github.com/spf13/viper"
19-
"gopkg.in/yaml.v3"
2019
)
2120

2221
var scriptCmd = &cobra.Command{
@@ -344,13 +343,22 @@ func parseScriptContent(content string, variables map[string]string) (*config.Co
344343
yamlLines = []string{} // Empty YAML
345344
}
346345

347-
// Parse YAML frontmatter
346+
// Parse YAML frontmatter using Viper for consistency with config file parsing
348347
var scriptConfig config.Config
349348
if len(yamlLines) > 0 {
350349
yamlContent := strings.Join(yamlLines, "\n")
351-
if err := yaml.Unmarshal([]byte(yamlContent), &scriptConfig); err != nil {
350+
351+
// Create temporary viper instance for frontmatter parsing
352+
frontmatterViper := viper.New()
353+
frontmatterViper.SetConfigType("yaml")
354+
355+
if err := frontmatterViper.ReadConfig(strings.NewReader(yamlContent)); err != nil {
352356
return nil, fmt.Errorf("failed to parse YAML frontmatter: %v\nYAML content:\n%s", err, yamlContent)
353357
}
358+
359+
if err := frontmatterViper.Unmarshal(&scriptConfig); err != nil {
360+
return nil, fmt.Errorf("failed to unmarshal frontmatter config: %v", err)
361+
}
354362
}
355363

356364
// Set prompt from content after frontmatter

cmd/script_integration_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ Working directory is ${env://WORK_DIR:-/tmp}.
6969
t.Fatal("GitHub server not found in script config")
7070
}
7171

72-
if githubServer.Environment["GITHUB_TOKEN"] != "ghp_test_token" {
73-
t.Errorf("Expected GITHUB_TOKEN=ghp_test_token, got %s", githubServer.Environment["GITHUB_TOKEN"])
72+
if githubServer.Environment["github_token"] != "ghp_test_token" {
73+
t.Errorf("Expected github_token=ghp_test_token, got %s", githubServer.Environment["github_token"])
7474
}
75-
if githubServer.Environment["DEBUG"] != "true" {
76-
t.Errorf("Expected DEBUG=true, got %s", githubServer.Environment["DEBUG"])
75+
if githubServer.Environment["debug"] != "true" {
76+
t.Errorf("Expected debug=true, got %s", githubServer.Environment["debug"])
7777
}
7878

7979
// Verify environment variable substitution in builtin server options

cmd/script_test.go

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,152 @@ Test prompt with compact mode`
298298
if len(config.MCPServers) != 1 {
299299
t.Errorf("Expected 1 MCP server, got %d", len(config.MCPServers))
300300
}
301+
}
302+
303+
func TestParseScriptContentMCPServersNewFormat(t *testing.T) {
304+
content := `---
305+
model: "anthropic:claude-sonnet-4-20250514"
306+
mcpServers:
307+
filesystem:
308+
type: "local"
309+
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
310+
environment:
311+
NODE_ENV: "production"
312+
remote-server:
313+
type: "remote"
314+
url: "https://example.com/mcp"
315+
builtin-todo:
316+
type: "builtin"
317+
name: "todo"
318+
options:
319+
storage: "memory"
320+
---
321+
Test prompt with new format MCP servers`
322+
323+
variables := make(map[string]string)
324+
config, err := parseScriptContent(content, variables)
325+
if err != nil {
326+
t.Fatalf("parseScriptContent() failed: %v", err)
327+
}
328+
329+
if len(config.MCPServers) != 3 {
330+
t.Errorf("Expected 3 MCP servers, got %d", len(config.MCPServers))
331+
}
332+
333+
// Test local server
334+
fs, exists := config.MCPServers["filesystem"]
335+
if !exists {
336+
t.Error("Expected filesystem server to exist")
337+
}
338+
if fs.Type != "local" {
339+
t.Errorf("Expected filesystem server type 'local', got '%s'", fs.Type)
340+
}
341+
expectedCommand := []string{"npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"}
342+
if len(fs.Command) != len(expectedCommand) {
343+
t.Errorf("Expected filesystem server command length %d, got %d", len(expectedCommand), len(fs.Command))
344+
}
345+
for i, expected := range expectedCommand {
346+
if i >= len(fs.Command) || fs.Command[i] != expected {
347+
t.Errorf("Expected filesystem server command %v, got %v", expectedCommand, fs.Command)
348+
break
349+
}
350+
}
351+
if fs.Environment["node_env"] != "production" {
352+
t.Errorf("Expected node_env=production, got %s", fs.Environment["node_env"])
353+
}
354+
355+
// Test remote server
356+
remote, exists := config.MCPServers["remote-server"]
357+
if !exists {
358+
t.Error("Expected remote-server to exist")
359+
}
360+
if remote.Type != "remote" {
361+
t.Errorf("Expected remote server type 'remote', got '%s'", remote.Type)
362+
}
363+
if remote.URL != "https://example.com/mcp" {
364+
t.Errorf("Expected remote server URL 'https://example.com/mcp', got '%s'", remote.URL)
365+
}
366+
367+
// Test builtin server
368+
builtin, exists := config.MCPServers["builtin-todo"]
369+
if !exists {
370+
t.Error("Expected builtin-todo server to exist")
371+
}
372+
if builtin.Type != "builtin" {
373+
t.Errorf("Expected builtin server type 'builtin', got '%s'", builtin.Type)
374+
}
375+
if builtin.Name != "todo" {
376+
t.Errorf("Expected builtin server name 'todo', got '%s'", builtin.Name)
377+
}
378+
}
379+
380+
func TestParseScriptContentMCPServersLegacyFormat(t *testing.T) {
381+
content := `---
382+
model: "anthropic:claude-sonnet-4-20250514"
383+
mcpServers:
384+
legacy-stdio:
385+
transport: "stdio"
386+
command: "npx"
387+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
388+
env:
389+
node_env: "development"
390+
legacy-sse:
391+
transport: "sse"
392+
url: "https://legacy.example.com/mcp"
393+
headers: ["Authorization: Bearer token"]
394+
---
395+
Test prompt with legacy format MCP servers`
301396

302-
if config.MCPServers["todo"].Type != "builtin" {
303-
t.Errorf("Expected todo server type 'builtin', got '%s'", config.MCPServers["todo"].Type)
397+
variables := make(map[string]string)
398+
config, err := parseScriptContent(content, variables)
399+
if err != nil {
400+
t.Fatalf("parseScriptContent() failed: %v", err)
401+
}
402+
403+
if len(config.MCPServers) != 2 {
404+
t.Errorf("Expected 2 MCP servers, got %d", len(config.MCPServers))
405+
}
406+
407+
// Test legacy stdio server - Note: Viper parsing doesn't trigger custom UnmarshalJSON
408+
// so legacy format has limited support in script frontmatter
409+
stdio, exists := config.MCPServers["legacy-stdio"]
410+
if !exists {
411+
t.Error("Expected legacy-stdio server to exist")
412+
}
413+
if stdio.Transport != "stdio" {
414+
t.Errorf("Expected legacy stdio transport 'stdio', got '%s'", stdio.Transport)
415+
}
416+
// Command field only gets the single command value, not combined with args
417+
if stdio.Command[0] != "npx" {
418+
t.Errorf("Expected legacy stdio command to start with 'npx', got %v", stdio.Command)
419+
}
420+
expectedArgs := []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"}
421+
if len(stdio.Args) != len(expectedArgs) {
422+
t.Errorf("Expected legacy stdio args length %d, got %d", len(expectedArgs), len(stdio.Args))
423+
}
424+
for i, expected := range expectedArgs {
425+
if i >= len(stdio.Args) || stdio.Args[i] != expected {
426+
t.Errorf("Expected legacy stdio args %v, got %v", expectedArgs, stdio.Args)
427+
break
428+
}
429+
}
430+
// Env field should contain the environment variables (with lowercase keys due to Viper)
431+
if stdio.Env["node_env"] != "development" {
432+
t.Errorf("Expected legacy stdio env node_env=development, got %v", stdio.Env["node_env"])
433+
}
434+
435+
// Test legacy SSE server
436+
sse, exists := config.MCPServers["legacy-sse"]
437+
if !exists {
438+
t.Error("Expected legacy-sse server to exist")
439+
}
440+
if sse.Transport != "sse" {
441+
t.Errorf("Expected legacy sse transport 'sse', got '%s'", sse.Transport)
442+
}
443+
if sse.URL != "https://legacy.example.com/mcp" {
444+
t.Errorf("Expected legacy sse URL 'https://legacy.example.com/mcp', got '%s'", sse.URL)
445+
}
446+
if len(sse.Headers) != 1 || sse.Headers[0] != "Authorization: Bearer token" {
447+
t.Errorf("Expected legacy sse headers [Authorization: Bearer token], got %v", sse.Headers)
304448
}
305449
}

0 commit comments

Comments
 (0)