Skip to content

Commit f1bb4a0

Browse files
authored
Return map from rule ID to MCPServerConfig in Result.MCPServers() (#201)
1 parent 0517d04 commit f1bb4a0

5 files changed

Lines changed: 198 additions & 25 deletions

File tree

pkg/codingcontext/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ func main() {
116116

117117
// Access MCP server configurations
118118
mcpServers := result.MCPServers()
119-
for i, config := range mcpServers {
120-
fmt.Printf("MCP Server %d: %s\n", i, config.Command)
119+
for id, config := range mcpServers {
120+
fmt.Printf("MCP Server %s: %s\n", id, config.Command)
121121
}
122122
}
123123
```
@@ -139,7 +139,7 @@ Result holds the assembled context from running a task:
139139
- `Agent Agent` - The agent used (from task frontmatter or option)
140140

141141
**Methods:**
142-
- `MCPServers() []MCPServerConfig` - Returns all MCP server configurations from rules as a slice
142+
- `MCPServers() map[string]MCPServerConfig` - Returns all MCP server configurations from rules as a map from rule ID to configuration
143143

144144
#### `Markdown[T]`
145145

pkg/codingcontext/context.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ func New(opts ...Option) *Context {
5656
return c
5757
}
5858

59+
// generateIDFromPath generates an ID from a file path by extracting the filename without extension.
60+
// Used to auto-set ID fields in frontmatter when not explicitly provided.
61+
func generateIDFromPath(path string) string {
62+
baseName := filepath.Base(path)
63+
ext := filepath.Ext(baseName)
64+
return strings.TrimSuffix(baseName, ext)
65+
}
66+
5967
type markdownVisitor func(path string, fm *markdown.BaseFrontMatter) error
6068

6169
// findMarkdownFile searches for a markdown file by name in the given directories.
@@ -129,6 +137,11 @@ func (cc *Context) findTask(taskName string) error {
129137
return fmt.Errorf("failed to parse task file %s: %w", path, err)
130138
}
131139

140+
// Automatically set ID to filename (without extension) if not set in frontmatter
141+
if frontMatter.ID == "" {
142+
frontMatter.ID = generateIDFromPath(path)
143+
}
144+
132145
// Extract selector labels from task frontmatter and add them to cc.includes.
133146
// This combines CLI selectors (from -s flag) with task selectors using OR logic:
134147
// rules match if their frontmatter value matches ANY selector value for a given key.
@@ -229,6 +242,11 @@ func (cc *Context) findCommand(commandName string, params taskparser.Params) (st
229242
return fmt.Errorf("failed to parse command file %s: %w", path, err)
230243
}
231244

245+
// Automatically set ID to filename (without extension) if not set in frontmatter
246+
if frontMatter.ID == "" {
247+
frontMatter.ID = generateIDFromPath(path)
248+
}
249+
232250
// Extract selector labels from command frontmatter and add them to cc.includes.
233251
// This combines CLI selectors, task selectors, and command selectors using OR logic:
234252
// rules match if their frontmatter value matches ANY selector value for a given key.
@@ -517,6 +535,11 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err
517535
return fmt.Errorf("failed to parse markdown file %s: %w", path, err)
518536
}
519537

538+
// Automatically set ID to filename (without extension) if not set in frontmatter
539+
if frontmatter.ID == "" {
540+
frontmatter.ID = generateIDFromPath(path)
541+
}
542+
520543
// Expand parameters only if expand is not explicitly set to false
521544
var processedContent string
522545
if shouldExpandParams(frontmatter.ExpandParams) {

pkg/codingcontext/context_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,32 @@ func TestContext_Run_Basic(t *testing.T) {
355355
}
356356
},
357357
},
358+
{
359+
name: "task ID automatically set from filename",
360+
setup: func(t *testing.T, dir string) {
361+
createTask(t, dir, "my-task", "", "Task content")
362+
},
363+
taskName: "my-task",
364+
wantErr: false,
365+
check: func(t *testing.T, result *Result) {
366+
if result.Task.FrontMatter.ID != "my-task" {
367+
t.Errorf("expected task ID 'my-task', got %q", result.Task.FrontMatter.ID)
368+
}
369+
},
370+
},
371+
{
372+
name: "task with explicit ID in frontmatter",
373+
setup: func(t *testing.T, dir string) {
374+
createTask(t, dir, "file-name", "id: explicit-task-id", "Task content")
375+
},
376+
taskName: "file-name",
377+
wantErr: false,
378+
check: func(t *testing.T, result *Result) {
379+
if result.Task.FrontMatter.ID != "explicit-task-id" {
380+
t.Errorf("expected task ID 'explicit-task-id', got %q", result.Task.FrontMatter.ID)
381+
}
382+
},
383+
},
358384
}
359385

360386
for _, tt := range tests {
@@ -699,6 +725,46 @@ func TestContext_Run_Rules(t *testing.T) {
699725
}
700726
},
701727
},
728+
{
729+
name: "rule IDs automatically set from filename",
730+
setup: func(t *testing.T, dir string) {
731+
createTask(t, dir, "id-task", "", "Task")
732+
createRule(t, dir, ".agents/rules/my-rule.md", "", "Rule without ID in frontmatter")
733+
createRule(t, dir, ".agents/rules/another-rule.md", "id: explicit-id", "Rule with explicit ID")
734+
},
735+
taskName: "id-task",
736+
wantErr: false,
737+
check: func(t *testing.T, result *Result) {
738+
if len(result.Rules) != 2 {
739+
t.Fatalf("expected 2 rules, got %d", len(result.Rules))
740+
}
741+
742+
// Check that one rule has auto-generated ID from filename
743+
foundMyRule := false
744+
foundAnotherRule := false
745+
for _, rule := range result.Rules {
746+
if rule.FrontMatter.ID == "my-rule" {
747+
foundMyRule = true
748+
if !strings.Contains(rule.Content, "Rule without ID") {
749+
t.Error("my-rule should contain 'Rule without ID'")
750+
}
751+
}
752+
if rule.FrontMatter.ID == "explicit-id" {
753+
foundAnotherRule = true
754+
if !strings.Contains(rule.Content, "Rule with explicit ID") {
755+
t.Error("explicit-id should contain 'Rule with explicit ID'")
756+
}
757+
}
758+
}
759+
760+
if !foundMyRule {
761+
t.Error("expected to find rule with auto-generated ID 'my-rule'")
762+
}
763+
if !foundAnotherRule {
764+
t.Error("expected to find rule with explicit ID 'explicit-id'")
765+
}
766+
},
767+
},
702768
}
703769

704770
for _, tt := range tests {

pkg/codingcontext/result.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ type Result struct {
1717
Prompt string // Combined prompt: all rules and task content
1818
}
1919

20-
// MCPServers returns all MCP server configurations from rules.
20+
// MCPServers returns all MCP server configurations from rules as a map.
2121
// Each rule can specify one MCP server configuration.
22-
// Returns a slice of all configured MCP servers from rules only.
22+
// Returns a map from rule ID to MCP server configuration.
2323
// Empty/zero-value MCP server configurations are filtered out.
24-
func (r *Result) MCPServers() []mcp.MCPServerConfig {
25-
var servers []mcp.MCPServerConfig
24+
// The rule ID is automatically set to the filename (without extension) if not
25+
// explicitly provided in the frontmatter.
26+
func (r *Result) MCPServers() map[string]mcp.MCPServerConfig {
27+
servers := make(map[string]mcp.MCPServerConfig)
2628

2729
// Add server from each rule, filtering out empty configs
2830
for _, rule := range r.Rules {
2931
server := rule.FrontMatter.MCPServer
3032
// Skip empty MCP server configs (no command and no URL means empty)
3133
if server.Command != "" || server.URL != "" {
32-
servers = append(servers, server)
34+
servers[rule.FrontMatter.ID] = server
3335
}
3436
}
3537

pkg/codingcontext/result_test.go

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestResult_MCPServers(t *testing.T) {
7676
tests := []struct {
7777
name string
7878
result Result
79-
want []mcp.MCPServerConfig
79+
want map[string]mcp.MCPServerConfig
8080
}{
8181
{
8282
name: "no MCP servers",
@@ -87,20 +87,22 @@ func TestResult_MCPServers(t *testing.T) {
8787
FrontMatter: markdown.TaskFrontMatter{},
8888
},
8989
},
90-
want: []mcp.MCPServerConfig{},
90+
want: map[string]mcp.MCPServerConfig{},
9191
},
9292
{
93-
name: "MCP servers from rules only",
93+
name: "MCP servers from rules with IDs",
9494
result: Result{
9595
Name: "test-task",
9696
Rules: []markdown.Markdown[markdown.RuleFrontMatter]{
9797
{
9898
FrontMatter: markdown.RuleFrontMatter{
99+
ID: "jira-server",
99100
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "jira"},
100101
},
101102
},
102103
{
103104
FrontMatter: markdown.RuleFrontMatter{
105+
ID: "api-server",
104106
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"},
105107
},
106108
},
@@ -109,9 +111,35 @@ func TestResult_MCPServers(t *testing.T) {
109111
FrontMatter: markdown.TaskFrontMatter{},
110112
},
111113
},
112-
want: []mcp.MCPServerConfig{
113-
{Type: mcp.TransportTypeStdio, Command: "jira"},
114-
{Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"},
114+
want: map[string]mcp.MCPServerConfig{
115+
"jira-server": {Type: mcp.TransportTypeStdio, Command: "jira"},
116+
"api-server": {Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"},
117+
},
118+
},
119+
{
120+
name: "MCP servers from rules without explicit IDs in frontmatter",
121+
result: Result{
122+
Rules: []markdown.Markdown[markdown.RuleFrontMatter]{
123+
{
124+
FrontMatter: markdown.RuleFrontMatter{
125+
ID: "rule-file-1", // ID is auto-set to filename during loading
126+
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"},
127+
},
128+
},
129+
{
130+
FrontMatter: markdown.RuleFrontMatter{
131+
ID: "rule-file-2", // ID is auto-set to filename during loading
132+
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"},
133+
},
134+
},
135+
},
136+
Task: markdown.Markdown[markdown.TaskFrontMatter]{
137+
FrontMatter: markdown.TaskFrontMatter{},
138+
},
139+
},
140+
want: map[string]mcp.MCPServerConfig{
141+
"rule-file-1": {Type: mcp.TransportTypeStdio, Command: "server1"},
142+
"rule-file-2": {Type: mcp.TransportTypeStdio, Command: "server2"},
115143
},
116144
},
117145
{
@@ -121,25 +149,29 @@ func TestResult_MCPServers(t *testing.T) {
121149
Rules: []markdown.Markdown[markdown.RuleFrontMatter]{
122150
{
123151
FrontMatter: markdown.RuleFrontMatter{
152+
ID: "server1-id",
124153
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"},
125154
},
126155
},
127156
{
128157
FrontMatter: markdown.RuleFrontMatter{
158+
ID: "server2-id",
129159
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"},
130160
},
131161
},
132162
{
133-
FrontMatter: markdown.RuleFrontMatter{},
163+
FrontMatter: markdown.RuleFrontMatter{
164+
ID: "empty-rule",
165+
},
134166
},
135167
},
136168
Task: markdown.Markdown[markdown.TaskFrontMatter]{
137169
FrontMatter: markdown.TaskFrontMatter{},
138170
},
139171
},
140-
want: []mcp.MCPServerConfig{
141-
{Type: mcp.TransportTypeStdio, Command: "server1"},
142-
{Type: mcp.TransportTypeStdio, Command: "server2"},
172+
want: map[string]mcp.MCPServerConfig{
173+
"server1-id": {Type: mcp.TransportTypeStdio, Command: "server1"},
174+
"server2-id": {Type: mcp.TransportTypeStdio, Command: "server2"},
143175
// Empty rule MCP server is filtered out
144176
},
145177
},
@@ -149,17 +181,52 @@ func TestResult_MCPServers(t *testing.T) {
149181
Name: "test-task",
150182
Rules: []markdown.Markdown[markdown.RuleFrontMatter]{
151183
{
152-
FrontMatter: markdown.RuleFrontMatter{},
184+
FrontMatter: markdown.RuleFrontMatter{
185+
ID: "no-server-rule",
186+
},
153187
},
154188
},
155189
Task: markdown.Markdown[markdown.TaskFrontMatter]{
156190
FrontMatter: markdown.TaskFrontMatter{},
157191
},
158192
},
159-
want: []mcp.MCPServerConfig{
193+
want: map[string]mcp.MCPServerConfig{
160194
// Empty rule MCP server is filtered out
161195
},
162196
},
197+
{
198+
name: "mixed rules with explicit and auto-generated IDs",
199+
result: Result{
200+
Rules: []markdown.Markdown[markdown.RuleFrontMatter]{
201+
{
202+
FrontMatter: markdown.RuleFrontMatter{
203+
ID: "explicit-id", // Explicit ID in frontmatter
204+
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"},
205+
},
206+
},
207+
{
208+
FrontMatter: markdown.RuleFrontMatter{
209+
ID: "some-rule", // ID auto-set to filename during loading
210+
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"},
211+
},
212+
},
213+
{
214+
FrontMatter: markdown.RuleFrontMatter{
215+
ID: "another-id", // Explicit ID in frontmatter
216+
MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeHTTP, URL: "https://example.com"},
217+
},
218+
},
219+
},
220+
Task: markdown.Markdown[markdown.TaskFrontMatter]{
221+
FrontMatter: markdown.TaskFrontMatter{},
222+
},
223+
},
224+
want: map[string]mcp.MCPServerConfig{
225+
"explicit-id": {Type: mcp.TransportTypeStdio, Command: "server1"},
226+
"some-rule": {Type: mcp.TransportTypeStdio, Command: "server2"},
227+
"another-id": {Type: mcp.TransportTypeHTTP, URL: "https://example.com"},
228+
},
229+
},
163230
}
164231

165232
for _, tt := range tests {
@@ -168,22 +235,37 @@ func TestResult_MCPServers(t *testing.T) {
168235

169236
if len(got) != len(tt.want) {
170237
t.Errorf("MCPServers() returned %d servers, want %d", len(got), len(tt.want))
238+
t.Logf("Got keys: %v", mapKeys(got))
239+
t.Logf("Want keys: %v", mapKeys(tt.want))
171240
return
172241
}
173242

174-
for i, wantServer := range tt.want {
175-
gotServer := got[i]
243+
for key, wantServer := range tt.want {
244+
gotServer, ok := got[key]
245+
if !ok {
246+
t.Errorf("MCPServers() missing key %q", key)
247+
continue
248+
}
176249

177250
if gotServer.Type != wantServer.Type {
178-
t.Errorf("MCPServers()[%d].Type = %v, want %v", i, gotServer.Type, wantServer.Type)
251+
t.Errorf("MCPServers()[%q].Type = %v, want %v", key, gotServer.Type, wantServer.Type)
179252
}
180253
if gotServer.Command != wantServer.Command {
181-
t.Errorf("MCPServers()[%d].Command = %q, want %q", i, gotServer.Command, wantServer.Command)
254+
t.Errorf("MCPServers()[%q].Command = %q, want %q", key, gotServer.Command, wantServer.Command)
182255
}
183256
if gotServer.URL != wantServer.URL {
184-
t.Errorf("MCPServers()[%d].URL = %q, want %q", i, gotServer.URL, wantServer.URL)
257+
t.Errorf("MCPServers()[%q].URL = %q, want %q", key, gotServer.URL, wantServer.URL)
185258
}
186259
}
187260
})
188261
}
189262
}
263+
264+
// Helper function to get map keys for debugging
265+
func mapKeys(m map[string]mcp.MCPServerConfig) []string {
266+
keys := make([]string, 0, len(m))
267+
for k := range m {
268+
keys = append(keys, k)
269+
}
270+
return keys
271+
}

0 commit comments

Comments
 (0)