internal/mcp/writes.go
1
package mcp
3
import (
4
"context"
5
"fmt"
6
"strings"
8
sdk "github.com/modelcontextprotocol/go-sdk/mcp"
10
"github.com/koment-dev/koment/internal/application"
11
"github.com/koment-dev/koment/internal/repository"
12
"github.com/koment-dev/koment/internal/store"
13
)
15
const (
16
addDescription = "Create one attributed koment annotation. Use this for local rationale instead of adding an explanatory inline comment. The author is the connected MCP client and is recorded as an agent."
17
reanchorDescription = "Explicitly confirm a new file or excerpt for an existing annotation while preserving its stable id, author and creation date."
18
convertDescription = "Convert one complete Go comment group into an attributed koment annotation. The annotation is written before the comment is removed from source."
19
acknowledgeDescription = "Retain one exceptional Go comment by creating an exact, attributable policy acknowledgement. acknowledge_inline_comment must be true; ordinary explanatory comments should use koment_convert_comment."
20
)
22
type AddInput struct {
23
Repository string `json:"repository,omitempty" jsonschema:"repository id; required when several repositories are served"`
24
File string `json:"file" jsonschema:"source path relative to the repository root"`
25
Excerpt string `json:"excerpt,omitempty" jsonschema:"verbatim code excerpt; omit for a file-scoped annotation"`
26
Kind string `json:"kind" jsonschema:"one of why, gotcha, invariant, anti-pattern"`
27
Title string `json:"title,omitempty" jsonschema:"short headline shown beside the code, at most 72 characters on one line; derived from the body when omitted"`
28
Body string `json:"body" jsonschema:"the rationale to record"`
29
}
31
type ReanchorInput struct {
32
Repository string `json:"repository,omitempty" jsonschema:"repository id; required when several repositories are served"`
33
ID string `json:"id" jsonschema:"stable annotation id"`
34
File string `json:"file,omitempty" jsonschema:"new repository-relative source path"`
35
Excerpt string `json:"excerpt,omitempty" jsonschema:"new verbatim code excerpt"`
36
}
38
type ConvertCommentInput struct {
39
Repository string `json:"repository,omitempty" jsonschema:"repository id; required when several repositories are served"`
40
File string `json:"file" jsonschema:"source path relative to the repository root"`
41
Comment string `json:"comment" jsonschema:"complete verbatim Go comment group"`
42
Kind string `json:"kind,omitempty" jsonschema:"annotation kind; defaults to why"`
43
}
45
type AcknowledgeCommentInput struct {
46
Repository string `json:"repository,omitempty" jsonschema:"repository id; required when several repositories are served"`
47
File string `json:"file" jsonschema:"source path relative to the repository root"`
48
Comment string `json:"comment" jsonschema:"complete verbatim Go comment group to retain"`
49
Body string `json:"body" jsonschema:"why the normal koment procedure is insufficient"`
50
Acknowledged bool `json:"acknowledge_inline_comment" jsonschema:"must be true to waive the normal koment procedure"`
51
}
53
type MutationOutput struct {
54
Repository string `json:"repository"`
55
Path string `json:"path"`
56
Record MutationRecord `json:"record"`
57
Warnings []string `json:"warnings,omitempty"`
58
Review *MutationReview `json:"review,omitempty"`
59
}
61
type MutationReview struct {
62
BaseCommit string `json:"base_commit"`
63
Branch string `json:"branch"`
64
Commit string `json:"commit"`
65
PullRequest int `json:"pull_request"`
66
URL string `json:"url"`
67
}
69
type MutationRecord struct {
70
APIVersion string `json:"api_version"`
71
ID string `json:"id"`
72
File string `json:"file"`
73
Kind string `json:"kind"`
74
Body string `json:"body"`
75
Created string `json:"created"`
76
Anchor MutationAnchor `json:"anchor"`
77
Git *AnnotationGit `json:"git,omitempty"`
78
Author AnnotationAuthor `json:"author"`
79
Policy *AnnotationPolicy `json:"policy,omitempty"`
80
}
82
type MutationAnchor struct {
83
Scope string `json:"scope"`
84
Excerpt string `json:"excerpt,omitempty"`
85
Before string `json:"before,omitempty"`
86
After string `json:"after,omitempty"`
87
LastSeenLine int `json:"last_seen_line,omitempty"`
88
}
90
func addWriteTools(server *sdk.Server, repositories *repository.Set) {
91
sdk.AddTool(server, &sdk.Tool{Name: "koment_add", Description: addDescription}, add(repositories))
92
sdk.AddTool(server, &sdk.Tool{Name: "koment_reanchor", Description: reanchorDescription}, reanchor(repositories))
93
sdk.AddTool(server, &sdk.Tool{Name: "koment_convert_comment", Description: convertDescription}, convertComment(repositories))
94
sdk.AddTool(server, &sdk.Tool{Name: "koment_acknowledge_comment", Description: acknowledgeDescription}, acknowledgeComment(repositories))
95
}
97
func add(repositories *repository.Set) sdk.ToolHandlerFor[AddInput, MutationOutput] {
98
return func(_ context.Context, request *sdk.CallToolRequest, input AddInput) (*sdk.CallToolResult, MutationOutput, error) {
99
entry, err := forWrite(repositories, input.Repository)
100
if err != nil {
101
return nil, MutationOutput{}, err
102
}
103
kind, err := store.ParseType(input.Kind)
104
if err != nil {
105
return nil, MutationOutput{}, err
106
}
107
mutation, err := application.NewService(entry).Add(application.AddInput{
108
File: input.File, Excerpt: input.Excerpt, Kind: kind, Title: input.Title,
109
Body: input.Body, Author: agentAuthor(request),
110
})
111
if err != nil {
112
return nil, MutationOutput{}, err
113
}
114
return nil, mutationOutput(entry.ID, mutation), nil
115
}
116
}
118
func reanchor(repositories *repository.Set) sdk.ToolHandlerFor[ReanchorInput, MutationOutput] {
119
return func(_ context.Context, _ *sdk.CallToolRequest, input ReanchorInput) (*sdk.CallToolResult, MutationOutput, error) {
120
if strings.TrimSpace(input.ID) == "" {
121
return nil, MutationOutput{}, fmt.Errorf("id must not be empty")
122
}
123
if input.File == "" && input.Excerpt == "" {
124
return nil, MutationOutput{}, fmt.Errorf("reanchor needs file, excerpt, or both")
125
}
126
entry, err := forWrite(repositories, input.Repository)
127
if err != nil {
128
return nil, MutationOutput{}, err
129
}
130
mutation, err := application.NewService(entry).Reanchor(application.ReanchorInput{
131
ID: input.ID, File: input.File, Excerpt: input.Excerpt,
132
})
133
if err != nil {
134
return nil, MutationOutput{}, err
135
}
136
return nil, mutationOutput(entry.ID, mutation), nil
137
}
138
}
140
func convertComment(repositories *repository.Set) sdk.ToolHandlerFor[ConvertCommentInput, MutationOutput] {
141
return func(_ context.Context, request *sdk.CallToolRequest, input ConvertCommentInput) (*sdk.CallToolResult, MutationOutput, error) {
142
entry, err := forWrite(repositories, input.Repository)
143
if err != nil {
144
return nil, MutationOutput{}, err
145
}
146
kind := store.TypeWhy
147
if input.Kind != "" {
148
if kind, err = store.ParseType(input.Kind); err != nil {
149
return nil, MutationOutput{}, err
150
}
151
}
152
mutation, err := application.NewService(entry).ConvertComment(application.ConvertCommentInput{
153
File: input.File, Comment: input.Comment, Kind: kind, Author: agentAuthor(request),
154
})
155
if err != nil {
156
return nil, MutationOutput{}, err
157
}
158
return nil, mutationOutput(entry.ID, mutation), nil
159
}
160
}
162
func acknowledgeComment(repositories *repository.Set) sdk.ToolHandlerFor[AcknowledgeCommentInput, MutationOutput] {
163
return func(_ context.Context, request *sdk.CallToolRequest, input AcknowledgeCommentInput) (*sdk.CallToolResult, MutationOutput, error) {
164
entry, err := forWrite(repositories, input.Repository)
165
if err != nil {
166
return nil, MutationOutput{}, err
167
}
168
mutation, err := application.NewService(entry).AcknowledgeComment(application.AcknowledgeCommentInput{
169
File: input.File, Comment: input.Comment, Body: input.Body,
170
Author: agentAuthor(request), Acknowledged: input.Acknowledged,
171
})
172
if err != nil {
173
return nil, MutationOutput{}, err
174
}
175
return nil, mutationOutput(entry.ID, mutation), nil
176
}
177
}
179
func forWrite(repositories *repository.Set, named string) (repository.Repository, error) {
180
if named != "" {
181
entry, found := repositories.Resolve(named)
182
if !found {
183
return repository.Repository{}, fmt.Errorf("no repository %q; served: %s", named, strings.Join(repositories.IDs(), ", "))
184
}
185
return entry, nil
186
}
187
if entry, only := repositories.Only(); only {
188
return entry, nil
189
}
190
return repository.Repository{}, fmt.Errorf("write requires repository; served: %s", strings.Join(repositories.IDs(), ", "))
191
}
193
func agentAuthor(request *sdk.CallToolRequest) store.Author {
194
name := "MCP agent"
195
if client := request.ClientInfo(); client != nil && strings.TrimSpace(client.Name) != "" {
196
name = client.Name
197
}
198
return store.Author{Name: name, Kind: store.AuthorAgent, Source: store.FromSession}
199
}
201
func mutationOutput(repositoryID string, mutation application.Mutation) MutationOutput {
202
record := mutation.Record
203
output := MutationOutput{
204
Repository: repositoryID, Path: mutation.Path, Warnings: mutation.Warnings,
205
Record: MutationRecord{
206
APIVersion: record.APIVersion, ID: record.Metadata.ID, File: record.Spec.Target.File, Kind: string(record.Spec.Type),
207
Body: record.Spec.Body, Created: record.Metadata.Created.Format("2006-01-02"),
208
Anchor: MutationAnchor{
209
Scope: string(record.Spec.Anchor.Scope), Excerpt: record.Spec.Anchor.Excerpt,
210
Before: record.Spec.Anchor.Before, After: record.Spec.Anchor.After, LastSeenLine: record.Status.LastSeenLine,
211
},
212
Author: AnnotationAuthor{
213
Name: record.Spec.Author.Name, Email: record.Spec.Author.Email, Kind: string(record.Spec.Author.Kind),
214
Source: string(record.Spec.Author.Source), Account: record.Spec.Author.Account, Verified: record.Spec.Author.Verified,
215
},
216
},
217
}
218
if record.Spec.Git != nil {
219
output.Record.Git = &AnnotationGit{
220
Commit: record.Spec.Git.Commit, Path: record.Spec.Git.Path, Line: record.Spec.Git.Line, EndLine: record.Spec.Git.EndLine,
221
}
222
}
223
if record.Spec.Policy != nil {
224
output.Record.Policy = &AnnotationPolicy{Exception: record.Spec.Policy.Exception, Acknowledged: record.Spec.Policy.Acknowledged}
225
}
226
return output
227
}