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
35 changes: 25 additions & 10 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,23 +146,33 @@ timeout: 10m

#### `mcp_servers` (optional, standard field)

**Type:** Array
**Purpose:** Specifies the list of MCP (Model Context Protocol) servers that the task should use; stored in frontmatter output but does not filter rules
**Type:** Map (from server name to server configuration)
**Purpose:** Specifies the MCP (Model Context Protocol) servers that the task should use; stored in frontmatter output but does not filter rules

The `mcp_servers` field is a **standard frontmatter field** following the industry standard for MCP server definition. It does not act as a selector.
The `mcp_servers` field is a **standard frontmatter field** following the industry standard for MCP server definition. It does not act as a selector. The field is a map where keys are server names and values are server configurations.

**Example:**
```yaml
---
task_name: file-operations
mcp_servers:
- filesystem
- git
- database
filesystem:
type: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
git:
type: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-git"]
database:
type: http
url: https://api.example.com/mcp
headers:
Authorization: Bearer token123
---
```

**Note:** The format follows the MCP specification for server identification.
**Note:** The format follows the MCP specification for server identification. Each server configuration includes a `type` field (e.g., "stdio", "http", "sse") and other fields specific to that transport type.

#### `agent` (optional, standard field)

Expand Down Expand Up @@ -436,13 +446,18 @@ agent: cursor

#### `mcp_servers` (rule metadata)

Specifies MCP servers that need to be running for this rule. Does not filter rules.
Specifies MCP servers that need to be running for this rule. Does not filter rules. The field is a map where keys are server names and values are server configurations.

```yaml
---
mcp_servers:
- filesystem
- database
filesystem:
type: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem"]
database:
type: http
url: https://api.example.com/mcp
---
# Metadata indicating required MCP servers
```
Expand Down
8 changes: 6 additions & 2 deletions examples/agents/tasks/example-with-standard-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ model: anthropic.claude-sonnet-4-20250514-v1-0
single_shot: false
timeout: 10m
mcp_servers:
- filesystem
- git
filesystem:
type: stdio
command: filesystem
git:
type: stdio
command: git
selectors:
stage: implementation
---
Expand Down
31 changes: 22 additions & 9 deletions pkg/codingcontext/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@ func TestParseTaskFile(t *testing.T) {
setupFiles: func(t *testing.T, tmpDir string) string {
taskPath := filepath.Join(tmpDir, "task.md")
createMarkdownFile(t, taskPath,
"task_name: test\nsingle_shot: true\ntimeout: 300\nmodel: gpt-4\nmcp_servers:\n - type: stdio\n command: server1\n - type: stdio\n command: server2",
"task_name: test\nsingle_shot: true\ntimeout: 300\nmodel: gpt-4\nmcp_servers:\n server1:\n type: stdio\n command: server1\n server2:\n type: stdio\n command: server2",
"# Task with Metadata Fields")
return taskPath
},
Expand All @@ -1216,7 +1216,7 @@ func TestParseTaskFile(t *testing.T) {
setupFiles: func(t *testing.T, tmpDir string) string {
taskPath := filepath.Join(tmpDir, "task.md")
createMarkdownFile(t, taskPath,
"task_name: test\nagent: cursor\nlanguage: go\nmodel: gpt-4\nsingle_shot: false\ntimeout: 10m\nmcp_servers:\n - type: stdio\n command: filesystem\n - type: stdio\n command: git",
"task_name: test\nagent: cursor\nlanguage: go\nmodel: gpt-4\nsingle_shot: false\ntimeout: 10m\nmcp_servers:\n filesystem:\n type: stdio\n command: filesystem\n git:\n type: stdio\n command: git",
"# Task with All Standard Fields")
return taskPath
},
Expand Down Expand Up @@ -2000,9 +2000,11 @@ model: anthropic.claude-sonnet-4-20250514-v1-0
single_shot: true
timeout: 5m
mcp_servers:
- type: stdio
filesystem:
type: stdio
command: filesystem-server
- type: stdio
git:
type: stdio
command: git-server`

taskPath := filepath.Join(tmpDir, ".agents", "tasks", "test-task.md")
Expand All @@ -2024,9 +2026,9 @@ mcp_servers:
"model": "anthropic.claude-sonnet-4-20250514-v1-0",
"single_shot": true,
"timeout": "5m",
"mcp_servers": []any{
map[string]any{"type": "stdio", "command": "filesystem-server"},
map[string]any{"type": "stdio", "command": "git-server"},
"mcp_servers": map[string]any{
"filesystem": map[string]any{"type": "stdio", "command": "filesystem-server"},
"git": map[string]any{"type": "stdio", "command": "git-server"},
},
}

Expand All @@ -2037,8 +2039,8 @@ mcp_servers:
continue
}

// Special handling for arrays
if field == "mcp_servers" || field == "languages" {
// Special handling for languages array
if field == "languages" {
actualArray, ok := actualValue.([]any)
if !ok {
t.Errorf("Expected %q to be []any, got %T", field, actualValue)
Expand All @@ -2048,6 +2050,17 @@ mcp_servers:
if len(actualArray) != len(expectedArray) {
t.Errorf("Expected %q length %d, got %d", field, len(expectedArray), len(actualArray))
}
} else if field == "mcp_servers" {
// Special handling for mcp_servers map
actualMap, ok := actualValue.(map[string]any)
if !ok {
t.Errorf("Expected %q to be map[string]any, got %T", field, actualValue)
continue
}
expectedMap := expectedValue.(map[string]any)
if len(actualMap) != len(expectedMap) {
t.Errorf("Expected %q length %d, got %d", field, len(expectedMap), len(actualMap))
}
} else {
// For simple values, just check they exist
// (exact comparison would require type matching which is complex with YAML)
Expand Down
3 changes: 3 additions & 0 deletions pkg/codingcontext/mcp_server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ type MCPServerConfig struct {
// Used for "http" and "sse" types.
Headers map[string]string `json:"headers,omitempty"`
}

// MCPServerConfigs maps server names to their configurations.
type MCPServerConfigs map[string]MCPServerConfig
26 changes: 15 additions & 11 deletions pkg/codingcontext/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,24 @@ type Result struct {
}

// MCPServers returns all MCP servers from both rules and the task.
// Servers from the task are included first, followed by servers from rules.
// Duplicate servers may be present if the same server is specified in multiple places.
func (r *Result) MCPServers() []MCPServerConfig {
var servers []MCPServerConfig
// Servers from the task take precedence over servers from rules.
// If multiple rules define the same server name, the behavior is non-deterministic.
func (r *Result) MCPServers() MCPServerConfigs {
servers := make(MCPServerConfigs)

// Add servers from task first
if r.Task.FrontMatter.MCPServers != nil {
servers = append(servers, r.Task.FrontMatter.MCPServers...)
}

// Add servers from all rules
// Add servers from rules first (so task can override)
for _, rule := range r.Rules {
if rule.FrontMatter.MCPServers != nil {
servers = append(servers, rule.FrontMatter.MCPServers...)
for name, config := range rule.FrontMatter.MCPServers {
servers[name] = config
}
}
}

// Add servers from task (overriding any from rules)
if r.Task.FrontMatter.MCPServers != nil {
for name, config := range r.Task.FrontMatter.MCPServers {
servers[name] = config
}
}

Expand Down
102 changes: 63 additions & 39 deletions pkg/codingcontext/result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ func TestResult_MCPServers(t *testing.T) {
tests := []struct {
name string
result Result
want []MCPServerConfig
want MCPServerConfigs
}{
{
name: "no MCP servers",
Expand All @@ -18,24 +18,24 @@ func TestResult_MCPServers(t *testing.T) {
FrontMatter: TaskFrontMatter{},
},
},
want: []MCPServerConfig{},
want: MCPServerConfigs{},
},
{
name: "MCP servers from task only",
result: Result{
Rules: []Markdown[RuleFrontMatter]{},
Task: Markdown[TaskFrontMatter]{
FrontMatter: TaskFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "filesystem"},
{Type: TransportTypeStdio, Command: "git"},
MCPServers: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
"git": {Type: TransportTypeStdio, Command: "git"},
},
},
},
},
want: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "filesystem"},
{Type: TransportTypeStdio, Command: "git"},
want: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
"git": {Type: TransportTypeStdio, Command: "git"},
},
},
{
Expand All @@ -44,15 +44,15 @@ func TestResult_MCPServers(t *testing.T) {
Rules: []Markdown[RuleFrontMatter]{
{
FrontMatter: RuleFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "jira"},
MCPServers: MCPServerConfigs{
"jira": {Type: TransportTypeStdio, Command: "jira"},
},
},
},
{
FrontMatter: RuleFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeHTTP, URL: "https://api.example.com"},
MCPServers: MCPServerConfigs{
"api": {Type: TransportTypeHTTP, URL: "https://api.example.com"},
},
},
},
Expand All @@ -61,9 +61,9 @@ func TestResult_MCPServers(t *testing.T) {
FrontMatter: TaskFrontMatter{},
},
},
want: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "jira"},
{Type: TransportTypeHTTP, URL: "https://api.example.com"},
want: MCPServerConfigs{
"jira": {Type: TransportTypeStdio, Command: "jira"},
"api": {Type: TransportTypeHTTP, URL: "https://api.example.com"},
},
},
{
Expand All @@ -72,23 +72,23 @@ func TestResult_MCPServers(t *testing.T) {
Rules: []Markdown[RuleFrontMatter]{
{
FrontMatter: RuleFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "jira"},
MCPServers: MCPServerConfigs{
"jira": {Type: TransportTypeStdio, Command: "jira"},
},
},
},
},
Task: Markdown[TaskFrontMatter]{
FrontMatter: TaskFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "filesystem"},
MCPServers: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
},
},
},
},
want: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "filesystem"},
{Type: TransportTypeStdio, Command: "jira"},
want: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
"jira": {Type: TransportTypeStdio, Command: "jira"},
},
},
{
Expand All @@ -97,15 +97,15 @@ func TestResult_MCPServers(t *testing.T) {
Rules: []Markdown[RuleFrontMatter]{
{
FrontMatter: RuleFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "server1"},
MCPServers: MCPServerConfigs{
"server1": {Type: TransportTypeStdio, Command: "server1"},
},
},
},
{
FrontMatter: RuleFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "server2"},
MCPServers: MCPServerConfigs{
"server2": {Type: TransportTypeStdio, Command: "server2"},
},
},
},
Expand All @@ -115,16 +115,40 @@ func TestResult_MCPServers(t *testing.T) {
},
Task: Markdown[TaskFrontMatter]{
FrontMatter: TaskFrontMatter{
MCPServers: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "task-server"},
MCPServers: MCPServerConfigs{
"task-server": {Type: TransportTypeStdio, Command: "task-server"},
},
},
},
},
want: []MCPServerConfig{
{Type: TransportTypeStdio, Command: "task-server"},
{Type: TransportTypeStdio, Command: "server1"},
{Type: TransportTypeStdio, Command: "server2"},
want: MCPServerConfigs{
"task-server": {Type: TransportTypeStdio, Command: "task-server"},
"server1": {Type: TransportTypeStdio, Command: "server1"},
"server2": {Type: TransportTypeStdio, Command: "server2"},
},
},
{
name: "task overrides rule server with same name",
result: Result{
Rules: []Markdown[RuleFrontMatter]{
{
FrontMatter: RuleFrontMatter{
MCPServers: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "rule-filesystem"},
},
},
},
},
Task: Markdown[TaskFrontMatter]{
FrontMatter: TaskFrontMatter{
MCPServers: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "task-filesystem"},
},
},
},
},
want: MCPServerConfigs{
"filesystem": {Type: TransportTypeStdio, Command: "task-filesystem"},
},
},
}
Expand All @@ -138,21 +162,21 @@ func TestResult_MCPServers(t *testing.T) {
return
}

for i, wantServer := range tt.want {
if i >= len(got) {
t.Errorf("MCPServers() missing server at index %d", i)
for name, wantServer := range tt.want {
gotServer, exists := got[name]
if !exists {
t.Errorf("MCPServers() missing server %q", name)
continue
}

gotServer := got[i]
if gotServer.Type != wantServer.Type {
t.Errorf("MCPServers()[%d].Type = %v, want %v", i, gotServer.Type, wantServer.Type)
t.Errorf("MCPServers()[%q].Type = %v, want %v", name, gotServer.Type, wantServer.Type)
}
if gotServer.Command != wantServer.Command {
t.Errorf("MCPServers()[%d].Command = %q, want %q", i, gotServer.Command, wantServer.Command)
t.Errorf("MCPServers()[%q].Command = %q, want %q", name, gotServer.Command, wantServer.Command)
}
if gotServer.URL != wantServer.URL {
t.Errorf("MCPServers()[%d].URL = %q, want %q", i, gotServer.URL, wantServer.URL)
t.Errorf("MCPServers()[%q].URL = %q, want %q", name, gotServer.URL, wantServer.URL)
}
}
})
Expand Down
Loading