Skip to content

Commit 52fbcce

Browse files
Copilotalexec
andauthored
Convert MCP servers from slice to map structure (#137)
* Initial plan * Change MCP servers from slice to map structure Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Simplify MCP server merge logic - allow non-deterministic duplicates Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Add MCPServerConfigs type alias for map[string]MCPServerConfig Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexec <1142830+alexec@users.noreply.github.com>
1 parent 5f9b9c7 commit 52fbcce

10 files changed

Lines changed: 159 additions & 91 deletions

File tree

docs/reference/file-formats.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,23 +146,33 @@ timeout: 10m
146146

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

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

152-
The `mcp_servers` field is a **standard frontmatter field** following the industry standard for MCP server definition. It does not act as a selector.
152+
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.
153153

154154
**Example:**
155155
```yaml
156156
---
157157
task_name: file-operations
158158
mcp_servers:
159-
- filesystem
160-
- git
161-
- database
159+
filesystem:
160+
type: stdio
161+
command: npx
162+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
163+
git:
164+
type: stdio
165+
command: npx
166+
args: ["-y", "@modelcontextprotocol/server-git"]
167+
database:
168+
type: http
169+
url: https://api.example.com/mcp
170+
headers:
171+
Authorization: Bearer token123
162172
---
163173
```
164174

165-
**Note:** The format follows the MCP specification for server identification.
175+
**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.
166176

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

@@ -436,13 +446,18 @@ agent: cursor
436446

437447
#### `mcp_servers` (rule metadata)
438448

439-
Specifies MCP servers that need to be running for this rule. Does not filter rules.
449+
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.
440450

441451
```yaml
442452
---
443453
mcp_servers:
444-
- filesystem
445-
- database
454+
filesystem:
455+
type: stdio
456+
command: npx
457+
args: ["-y", "@modelcontextprotocol/server-filesystem"]
458+
database:
459+
type: http
460+
url: https://api.example.com/mcp
446461
---
447462
# Metadata indicating required MCP servers
448463
```

examples/agents/tasks/example-with-standard-fields.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ model: anthropic.claude-sonnet-4-20250514-v1-0
66
single_shot: false
77
timeout: 10m
88
mcp_servers:
9-
- filesystem
10-
- git
9+
filesystem:
10+
type: stdio
11+
command: filesystem
12+
git:
13+
type: stdio
14+
command: git
1115
selectors:
1216
stage: implementation
1317
---

pkg/codingcontext/context_test.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,7 +1200,7 @@ func TestParseTaskFile(t *testing.T) {
12001200
setupFiles: func(t *testing.T, tmpDir string) string {
12011201
taskPath := filepath.Join(tmpDir, "task.md")
12021202
createMarkdownFile(t, taskPath,
1203-
"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",
1203+
"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",
12041204
"# Task with Metadata Fields")
12051205
return taskPath
12061206
},
@@ -1216,7 +1216,7 @@ func TestParseTaskFile(t *testing.T) {
12161216
setupFiles: func(t *testing.T, tmpDir string) string {
12171217
taskPath := filepath.Join(tmpDir, "task.md")
12181218
createMarkdownFile(t, taskPath,
1219-
"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",
1219+
"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",
12201220
"# Task with All Standard Fields")
12211221
return taskPath
12221222
},
@@ -2000,9 +2000,11 @@ model: anthropic.claude-sonnet-4-20250514-v1-0
20002000
single_shot: true
20012001
timeout: 5m
20022002
mcp_servers:
2003-
- type: stdio
2003+
filesystem:
2004+
type: stdio
20042005
command: filesystem-server
2005-
- type: stdio
2006+
git:
2007+
type: stdio
20062008
command: git-server`
20072009

20082010
taskPath := filepath.Join(tmpDir, ".agents", "tasks", "test-task.md")
@@ -2024,9 +2026,9 @@ mcp_servers:
20242026
"model": "anthropic.claude-sonnet-4-20250514-v1-0",
20252027
"single_shot": true,
20262028
"timeout": "5m",
2027-
"mcp_servers": []any{
2028-
map[string]any{"type": "stdio", "command": "filesystem-server"},
2029-
map[string]any{"type": "stdio", "command": "git-server"},
2029+
"mcp_servers": map[string]any{
2030+
"filesystem": map[string]any{"type": "stdio", "command": "filesystem-server"},
2031+
"git": map[string]any{"type": "stdio", "command": "git-server"},
20302032
},
20312033
}
20322034

@@ -2037,8 +2039,8 @@ mcp_servers:
20372039
continue
20382040
}
20392041

2040-
// Special handling for arrays
2041-
if field == "mcp_servers" || field == "languages" {
2042+
// Special handling for languages array
2043+
if field == "languages" {
20422044
actualArray, ok := actualValue.([]any)
20432045
if !ok {
20442046
t.Errorf("Expected %q to be []any, got %T", field, actualValue)
@@ -2048,6 +2050,17 @@ mcp_servers:
20482050
if len(actualArray) != len(expectedArray) {
20492051
t.Errorf("Expected %q length %d, got %d", field, len(expectedArray), len(actualArray))
20502052
}
2053+
} else if field == "mcp_servers" {
2054+
// Special handling for mcp_servers map
2055+
actualMap, ok := actualValue.(map[string]any)
2056+
if !ok {
2057+
t.Errorf("Expected %q to be map[string]any, got %T", field, actualValue)
2058+
continue
2059+
}
2060+
expectedMap := expectedValue.(map[string]any)
2061+
if len(actualMap) != len(expectedMap) {
2062+
t.Errorf("Expected %q length %d, got %d", field, len(expectedMap), len(actualMap))
2063+
}
20512064
} else {
20522065
// For simple values, just check they exist
20532066
// (exact comparison would require type matching which is complex with YAML)

pkg/codingcontext/mcp_server_config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ type MCPServerConfig struct {
2424
// Used for "http" and "sse" types.
2525
Headers map[string]string `json:"headers,omitempty"`
2626
}
27+
28+
// MCPServerConfigs maps server names to their configurations.
29+
type MCPServerConfigs map[string]MCPServerConfig

pkg/codingcontext/result.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,24 @@ type Result struct {
2020
}
2121

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

28-
// Add servers from task first
29-
if r.Task.FrontMatter.MCPServers != nil {
30-
servers = append(servers, r.Task.FrontMatter.MCPServers...)
31-
}
32-
33-
// Add servers from all rules
28+
// Add servers from rules first (so task can override)
3429
for _, rule := range r.Rules {
3530
if rule.FrontMatter.MCPServers != nil {
36-
servers = append(servers, rule.FrontMatter.MCPServers...)
31+
for name, config := range rule.FrontMatter.MCPServers {
32+
servers[name] = config
33+
}
34+
}
35+
}
36+
37+
// Add servers from task (overriding any from rules)
38+
if r.Task.FrontMatter.MCPServers != nil {
39+
for name, config := range r.Task.FrontMatter.MCPServers {
40+
servers[name] = config
3741
}
3842
}
3943

pkg/codingcontext/result_test.go

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ func TestResult_MCPServers(t *testing.T) {
88
tests := []struct {
99
name string
1010
result Result
11-
want []MCPServerConfig
11+
want MCPServerConfigs
1212
}{
1313
{
1414
name: "no MCP servers",
@@ -18,24 +18,24 @@ func TestResult_MCPServers(t *testing.T) {
1818
FrontMatter: TaskFrontMatter{},
1919
},
2020
},
21-
want: []MCPServerConfig{},
21+
want: MCPServerConfigs{},
2222
},
2323
{
2424
name: "MCP servers from task only",
2525
result: Result{
2626
Rules: []Markdown[RuleFrontMatter]{},
2727
Task: Markdown[TaskFrontMatter]{
2828
FrontMatter: TaskFrontMatter{
29-
MCPServers: []MCPServerConfig{
30-
{Type: TransportTypeStdio, Command: "filesystem"},
31-
{Type: TransportTypeStdio, Command: "git"},
29+
MCPServers: MCPServerConfigs{
30+
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
31+
"git": {Type: TransportTypeStdio, Command: "git"},
3232
},
3333
},
3434
},
3535
},
36-
want: []MCPServerConfig{
37-
{Type: TransportTypeStdio, Command: "filesystem"},
38-
{Type: TransportTypeStdio, Command: "git"},
36+
want: MCPServerConfigs{
37+
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
38+
"git": {Type: TransportTypeStdio, Command: "git"},
3939
},
4040
},
4141
{
@@ -44,15 +44,15 @@ func TestResult_MCPServers(t *testing.T) {
4444
Rules: []Markdown[RuleFrontMatter]{
4545
{
4646
FrontMatter: RuleFrontMatter{
47-
MCPServers: []MCPServerConfig{
48-
{Type: TransportTypeStdio, Command: "jira"},
47+
MCPServers: MCPServerConfigs{
48+
"jira": {Type: TransportTypeStdio, Command: "jira"},
4949
},
5050
},
5151
},
5252
{
5353
FrontMatter: RuleFrontMatter{
54-
MCPServers: []MCPServerConfig{
55-
{Type: TransportTypeHTTP, URL: "https://api.example.com"},
54+
MCPServers: MCPServerConfigs{
55+
"api": {Type: TransportTypeHTTP, URL: "https://api.example.com"},
5656
},
5757
},
5858
},
@@ -61,9 +61,9 @@ func TestResult_MCPServers(t *testing.T) {
6161
FrontMatter: TaskFrontMatter{},
6262
},
6363
},
64-
want: []MCPServerConfig{
65-
{Type: TransportTypeStdio, Command: "jira"},
66-
{Type: TransportTypeHTTP, URL: "https://api.example.com"},
64+
want: MCPServerConfigs{
65+
"jira": {Type: TransportTypeStdio, Command: "jira"},
66+
"api": {Type: TransportTypeHTTP, URL: "https://api.example.com"},
6767
},
6868
},
6969
{
@@ -72,23 +72,23 @@ func TestResult_MCPServers(t *testing.T) {
7272
Rules: []Markdown[RuleFrontMatter]{
7373
{
7474
FrontMatter: RuleFrontMatter{
75-
MCPServers: []MCPServerConfig{
76-
{Type: TransportTypeStdio, Command: "jira"},
75+
MCPServers: MCPServerConfigs{
76+
"jira": {Type: TransportTypeStdio, Command: "jira"},
7777
},
7878
},
7979
},
8080
},
8181
Task: Markdown[TaskFrontMatter]{
8282
FrontMatter: TaskFrontMatter{
83-
MCPServers: []MCPServerConfig{
84-
{Type: TransportTypeStdio, Command: "filesystem"},
83+
MCPServers: MCPServerConfigs{
84+
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
8585
},
8686
},
8787
},
8888
},
89-
want: []MCPServerConfig{
90-
{Type: TransportTypeStdio, Command: "filesystem"},
91-
{Type: TransportTypeStdio, Command: "jira"},
89+
want: MCPServerConfigs{
90+
"filesystem": {Type: TransportTypeStdio, Command: "filesystem"},
91+
"jira": {Type: TransportTypeStdio, Command: "jira"},
9292
},
9393
},
9494
{
@@ -97,15 +97,15 @@ func TestResult_MCPServers(t *testing.T) {
9797
Rules: []Markdown[RuleFrontMatter]{
9898
{
9999
FrontMatter: RuleFrontMatter{
100-
MCPServers: []MCPServerConfig{
101-
{Type: TransportTypeStdio, Command: "server1"},
100+
MCPServers: MCPServerConfigs{
101+
"server1": {Type: TransportTypeStdio, Command: "server1"},
102102
},
103103
},
104104
},
105105
{
106106
FrontMatter: RuleFrontMatter{
107-
MCPServers: []MCPServerConfig{
108-
{Type: TransportTypeStdio, Command: "server2"},
107+
MCPServers: MCPServerConfigs{
108+
"server2": {Type: TransportTypeStdio, Command: "server2"},
109109
},
110110
},
111111
},
@@ -115,16 +115,40 @@ func TestResult_MCPServers(t *testing.T) {
115115
},
116116
Task: Markdown[TaskFrontMatter]{
117117
FrontMatter: TaskFrontMatter{
118-
MCPServers: []MCPServerConfig{
119-
{Type: TransportTypeStdio, Command: "task-server"},
118+
MCPServers: MCPServerConfigs{
119+
"task-server": {Type: TransportTypeStdio, Command: "task-server"},
120120
},
121121
},
122122
},
123123
},
124-
want: []MCPServerConfig{
125-
{Type: TransportTypeStdio, Command: "task-server"},
126-
{Type: TransportTypeStdio, Command: "server1"},
127-
{Type: TransportTypeStdio, Command: "server2"},
124+
want: MCPServerConfigs{
125+
"task-server": {Type: TransportTypeStdio, Command: "task-server"},
126+
"server1": {Type: TransportTypeStdio, Command: "server1"},
127+
"server2": {Type: TransportTypeStdio, Command: "server2"},
128+
},
129+
},
130+
{
131+
name: "task overrides rule server with same name",
132+
result: Result{
133+
Rules: []Markdown[RuleFrontMatter]{
134+
{
135+
FrontMatter: RuleFrontMatter{
136+
MCPServers: MCPServerConfigs{
137+
"filesystem": {Type: TransportTypeStdio, Command: "rule-filesystem"},
138+
},
139+
},
140+
},
141+
},
142+
Task: Markdown[TaskFrontMatter]{
143+
FrontMatter: TaskFrontMatter{
144+
MCPServers: MCPServerConfigs{
145+
"filesystem": {Type: TransportTypeStdio, Command: "task-filesystem"},
146+
},
147+
},
148+
},
149+
},
150+
want: MCPServerConfigs{
151+
"filesystem": {Type: TransportTypeStdio, Command: "task-filesystem"},
128152
},
129153
},
130154
}
@@ -138,21 +162,21 @@ func TestResult_MCPServers(t *testing.T) {
138162
return
139163
}
140164

141-
for i, wantServer := range tt.want {
142-
if i >= len(got) {
143-
t.Errorf("MCPServers() missing server at index %d", i)
165+
for name, wantServer := range tt.want {
166+
gotServer, exists := got[name]
167+
if !exists {
168+
t.Errorf("MCPServers() missing server %q", name)
144169
continue
145170
}
146171

147-
gotServer := got[i]
148172
if gotServer.Type != wantServer.Type {
149-
t.Errorf("MCPServers()[%d].Type = %v, want %v", i, gotServer.Type, wantServer.Type)
173+
t.Errorf("MCPServers()[%q].Type = %v, want %v", name, gotServer.Type, wantServer.Type)
150174
}
151175
if gotServer.Command != wantServer.Command {
152-
t.Errorf("MCPServers()[%d].Command = %q, want %q", i, gotServer.Command, wantServer.Command)
176+
t.Errorf("MCPServers()[%q].Command = %q, want %q", name, gotServer.Command, wantServer.Command)
153177
}
154178
if gotServer.URL != wantServer.URL {
155-
t.Errorf("MCPServers()[%d].URL = %q, want %q", i, gotServer.URL, wantServer.URL)
179+
t.Errorf("MCPServers()[%q].URL = %q, want %q", name, gotServer.URL, wantServer.URL)
156180
}
157181
}
158182
})

0 commit comments

Comments
 (0)