Skip to content

Commit ec0a079

Browse files
committed
add mcp for generating rules
1 parent f0d52fb commit ec0a079

4 files changed

Lines changed: 262 additions & 32 deletions

File tree

cmd/mcp.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package cmd
2+
3+
import (
4+
util "github.com/adr/ad-guidance-tool/internal/adapter/command"
5+
adgmcp "github.com/adr/ad-guidance-tool/internal/adapter/mcp"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func init() {
10+
rootCmd.AddCommand(newMCPCommand())
11+
}
12+
13+
func newMCPCommand() *cobra.Command {
14+
var modelPath string
15+
16+
c := &cobra.Command{
17+
Use: "mcp",
18+
Short: "Start the ADG MCP server for AI tool integration",
19+
Hidden: true,
20+
Long: `Start the ADG MCP server for AI tool integration.
21+
22+
The server communicates over stdio using the Model Context Protocol (MCP) and
23+
provides AI assistants (e.g. VS Code Copilot, Claude Desktop) with tools to
24+
read ADRs, access the ADE rule DSL reference, and browse existing rule files.
25+
26+
Configure it in your AI tool's MCP settings, for example in .vscode/mcp.json:
27+
28+
{
29+
"servers": {
30+
"adg": {
31+
"command": "adg",
32+
"args": ["mcp", "--model", "./docs/adr"]
33+
}
34+
}
35+
}
36+
37+
If --model is omitted, the default model path from the adg config is used.`,
38+
RunE: func(cmd *cobra.Command, args []string) error {
39+
resolvedPath, err := util.ResolveModelPathOrDefault(modelPath, configSvc)
40+
if err != nil {
41+
return err
42+
}
43+
return adgmcp.Serve(resolvedPath, decisionSvc)
44+
},
45+
}
46+
47+
c.Flags().StringVar(&modelPath, "model", "", "Path to the decision model (optional if configured)")
48+
return c
49+
}

go.mod

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
module github.com/adr/ad-guidance-tool
22

3-
go 1.25.0
3+
go 1.25.5
44

55
require (
6-
github.com/phi42/ad-enforcement-tool v0.1.0
6+
github.com/mark3labs/mcp-go v0.54.1
7+
github.com/phi42/ad-enforcement-tool v0.1.0-dev
78
github.com/spf13/cobra v1.10.2
89
github.com/spf13/viper v1.21.0
910
github.com/stretchr/testify v1.11.1
@@ -12,28 +13,25 @@ require (
1213

1314
require (
1415
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
15-
github.com/arch-go/arch-go v1.7.0 // indirect
1616
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
17-
github.com/fatih/color v1.18.0 // indirect
1817
github.com/fsnotify/fsnotify v1.10.1 // indirect
1918
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
19+
github.com/google/jsonschema-go v0.4.2 // indirect
20+
github.com/google/uuid v1.6.0 // indirect
2021
github.com/inconshreveable/mousetrap v1.1.0 // indirect
21-
github.com/mattn/go-colorable v0.1.13 // indirect
22-
github.com/mattn/go-isatty v0.0.20 // indirect
2322
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
2423
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2524
github.com/sagikazarmark/locafero v0.12.0 // indirect
25+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
2626
github.com/spf13/afero v1.15.0 // indirect
2727
github.com/spf13/cast v1.10.0 // indirect
2828
github.com/spf13/pflag v1.0.10 // indirect
2929
github.com/stretchr/objx v0.5.3 // indirect
3030
github.com/subosito/gotenv v1.6.0 // indirect
31+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
3132
go.yaml.in/yaml/v3 v3.0.4 // indirect
3233
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
33-
golang.org/x/mod v0.36.0 // indirect
34-
golang.org/x/sync v0.20.0 // indirect
3534
golang.org/x/sys v0.44.0 // indirect
3635
golang.org/x/text v0.37.0 // indirect
37-
golang.org/x/tools v0.45.0 // indirect
3836
google.golang.org/protobuf v1.36.11 // indirect
3937
)

go.sum

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
22
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
3-
github.com/arch-go/arch-go v1.7.0 h1:tiUJX5E/2RmZCmO7U+3YD+EvghryR8X/MBoQ2aeg/Jc=
4-
github.com/arch-go/arch-go v1.7.0/go.mod h1:b35wChRB+99iDDHNFXirsSOWXd8kdNcOJFqI4iKHKq4=
53
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
64
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
75
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8-
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
9-
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
6+
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
7+
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
108
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
119
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1210
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
@@ -15,28 +13,31 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
1513
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
1614
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
1715
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
16+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
17+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
18+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
19+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
1820
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
1921
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
2022
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
2123
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
2224
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
2325
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
24-
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
25-
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
26-
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
27-
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
28-
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
26+
github.com/mark3labs/mcp-go v0.54.1 h1:Ap/ptEB9FtWzFKM8NDsTA7QDxerQOC06eZigrTldVj0=
27+
github.com/mark3labs/mcp-go v0.54.1/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
2928
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
3029
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
31-
github.com/phi42/ad-enforcement-tool v0.1.0 h1:eVhCNIym9cIJYNPPDWrYFR1yetfvvg2fhRNHaG5EHYU=
32-
github.com/phi42/ad-enforcement-tool v0.1.0/go.mod h1:QF7b66wUMB4Ig+8prbrZLzejxpZ9cZS1nHkTDZg2Yrk=
30+
github.com/phi42/ad-enforcement-tool v0.1.0-dev h1:SAOXSNI4NX4w+qKTnuZyDDzC7j506Ds8DEZKKBu7B9g=
31+
github.com/phi42/ad-enforcement-tool v0.1.0-dev/go.mod h1:QF7b66wUMB4Ig+8prbrZLzejxpZ9cZS1nHkTDZg2Yrk=
3332
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
3433
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
35-
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
36-
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
34+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
35+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
3736
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
3837
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
3938
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
39+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
40+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
4041
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
4142
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
4243
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -54,26 +55,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
5455
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
5556
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
5657
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
58+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
59+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
5760
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
5861
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
5962
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
6063
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
61-
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
62-
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
63-
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
64-
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
65-
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
66-
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6764
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
6865
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
6966
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
7067
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
71-
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
72-
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
7368
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
7469
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
7570
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
76-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
77-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
71+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
72+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
7873
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
7974
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/adapter/mcp/server.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
aderule "github.com/phi42/ad-enforcement-tool/dslref"
11+
decisiondomain "github.com/adr/ad-guidance-tool/internal/domain/decision"
12+
mcplib "github.com/mark3labs/mcp-go/mcp"
13+
mcpserver "github.com/mark3labs/mcp-go/server"
14+
)
15+
16+
// Serve starts the ADG MCP server over stdio.
17+
func Serve(modelPath string, decisionSvc decisiondomain.DecisionService) error {
18+
s := buildServer(modelPath, decisionSvc)
19+
return mcpserver.ServeStdio(s)
20+
}
21+
22+
func buildServer(modelPath string, decisionSvc decisiondomain.DecisionService) *mcpserver.MCPServer {
23+
s := mcpserver.NewMCPServer(
24+
"adg",
25+
"1.0.0",
26+
mcpserver.WithInstructions(`ADG (Architectural Decision Guidance) MCP server.
27+
Use these tools to read architectural decision records (ADRs) and generate .rule files that enforce them.
28+
Always call get_dsl_reference before writing any .rule file content to ensure correct syntax.`),
29+
)
30+
31+
s.AddTool(
32+
mcplib.NewTool("list_adrs",
33+
mcplib.WithDescription("List all architectural decision records (ADRs) in the decision model. Returns each ADR's ID, title, and status."),
34+
mcplib.WithReadOnlyHintAnnotation(true),
35+
),
36+
listADRsHandler(modelPath, decisionSvc),
37+
)
38+
39+
s.AddTool(
40+
mcplib.NewTool("get_adr",
41+
mcplib.WithDescription("Get the full content of an ADR by its ID. Returns the markdown content and the path where the corresponding .rule file should be placed."),
42+
mcplib.WithString("id",
43+
mcplib.Required(),
44+
mcplib.Description(`The ADR ID, e.g. "0001"`),
45+
),
46+
mcplib.WithReadOnlyHintAnnotation(true),
47+
),
48+
getADRHandler(modelPath, decisionSvc),
49+
)
50+
51+
s.AddTool(
52+
mcplib.NewTool("get_dsl_reference",
53+
mcplib.WithDescription("Get the full ADE rule DSL language reference. Always read this before writing .rule file content."),
54+
mcplib.WithReadOnlyHintAnnotation(true),
55+
),
56+
getDSLReferenceHandler(),
57+
)
58+
59+
s.AddTool(
60+
mcplib.NewTool("list_rule_files",
61+
mcplib.WithDescription("List all existing .rule files in the decision model directory with their full content. Use these as examples when generating new rule files."),
62+
mcplib.WithReadOnlyHintAnnotation(true),
63+
),
64+
listRuleFilesHandler(modelPath),
65+
)
66+
67+
s.AddTool(
68+
mcplib.NewTool("validate_rule",
69+
mcplib.WithDescription("Validate the syntax and semantics of rule file content. Call this after generating a .rule file to confirm it is correct before presenting it to the user."),
70+
mcplib.WithString("content",
71+
mcplib.Required(),
72+
mcplib.Description("The full .rule file content to validate."),
73+
),
74+
),
75+
validateRuleHandler(),
76+
)
77+
78+
return s
79+
}
80+
81+
func listADRsHandler(modelPath string, decisionSvc decisiondomain.DecisionService) mcpserver.ToolHandlerFunc {
82+
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
83+
decisions, err := decisionSvc.GetAllDecisions(modelPath)
84+
if err != nil {
85+
return mcplib.NewToolResultError(fmt.Sprintf("failed to list ADRs: %v", err)), nil
86+
}
87+
88+
if len(decisions) == 0 {
89+
return mcplib.NewToolResultText("No ADRs found in the decision model."), nil
90+
}
91+
92+
var sb strings.Builder
93+
for _, d := range decisions {
94+
fmt.Fprintf(&sb, "- ID: %s | Title: %s | Status: %s\n", d.ID, d.Title, d.Status)
95+
}
96+
return mcplib.NewToolResultText(sb.String()), nil
97+
}
98+
}
99+
100+
func getADRHandler(modelPath string, decisionSvc decisiondomain.DecisionService) mcpserver.ToolHandlerFunc {
101+
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
102+
id, err := req.RequireString("id")
103+
if err != nil {
104+
return mcplib.NewToolResultError(`parameter "id" is required`), nil
105+
}
106+
107+
decision, err := decisionSvc.GetDecisionByID(modelPath, id)
108+
if err != nil {
109+
return mcplib.NewToolResultError(fmt.Sprintf("ADR %q not found: %v", id, err)), nil
110+
}
111+
112+
adrPath, err := decisionSvc.GetDecisionFilePath(modelPath, decision.ID)
113+
if err != nil {
114+
return mcplib.NewToolResultError(fmt.Sprintf("failed to resolve ADR file path: %v", err)), nil
115+
}
116+
117+
content, err := os.ReadFile(adrPath)
118+
if err != nil {
119+
return mcplib.NewToolResultError(fmt.Sprintf("failed to read ADR file: %v", err)), nil
120+
}
121+
122+
base := strings.TrimSuffix(filepath.Base(adrPath), filepath.Ext(adrPath))
123+
ruleFilePath := filepath.Join(filepath.Dir(adrPath), base+".rule")
124+
125+
ruleFileStatus := "does not exist yet (create it at: " + ruleFilePath + ")"
126+
if _, statErr := os.Stat(ruleFilePath); statErr == nil {
127+
ruleFileStatus = "already exists at: " + ruleFilePath
128+
}
129+
130+
var sb strings.Builder
131+
fmt.Fprintf(&sb, "ADR ID: %s\n", decision.ID)
132+
fmt.Fprintf(&sb, "Title: %s\n", decision.Title)
133+
fmt.Fprintf(&sb, "Status: %s\n", decision.Status)
134+
fmt.Fprintf(&sb, "File: %s\n", adrPath)
135+
fmt.Fprintf(&sb, "Rule file: %s\n\n", ruleFileStatus)
136+
fmt.Fprintf(&sb, "=== Content ===\n%s", string(content))
137+
138+
return mcplib.NewToolResultText(sb.String()), nil
139+
}
140+
}
141+
142+
func getDSLReferenceHandler() mcpserver.ToolHandlerFunc {
143+
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
144+
return mcplib.NewToolResultText(aderule.Reference), nil
145+
}
146+
}
147+
148+
func validateRuleHandler() mcpserver.ToolHandlerFunc {
149+
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
150+
content, err := req.RequireString("content")
151+
if err != nil {
152+
return mcplib.NewToolResultError(`parameter "content" is required`), nil
153+
}
154+
155+
if err := aderule.Validate(content); err != nil {
156+
return mcplib.NewToolResultError(fmt.Sprintf("Validation failed:\n%v", err)), nil
157+
}
158+
return mcplib.NewToolResultText("\u2713 Rule is valid."), nil
159+
}
160+
}
161+
162+
func listRuleFilesHandler(modelPath string) mcpserver.ToolHandlerFunc {
163+
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
164+
entries, err := os.ReadDir(modelPath)
165+
if err != nil {
166+
return mcplib.NewToolResultError(fmt.Sprintf("failed to read model directory: %v", err)), nil
167+
}
168+
169+
var sb strings.Builder
170+
count := 0
171+
for _, entry := range entries {
172+
if entry.IsDir() || filepath.Ext(entry.Name()) != ".rule" {
173+
continue
174+
}
175+
content, err := os.ReadFile(filepath.Join(modelPath, entry.Name()))
176+
if err != nil {
177+
continue
178+
}
179+
fmt.Fprintf(&sb, "=== %s ===\n%s\n", entry.Name(), string(content))
180+
count++
181+
}
182+
183+
if count == 0 {
184+
return mcplib.NewToolResultText("No .rule files found in the model directory."), nil
185+
}
186+
return mcplib.NewToolResultText(sb.String()), nil
187+
}
188+
}

0 commit comments

Comments
 (0)