snapshot of 962fc3c82c774ab371e3fc4e84c55ef26e61268d Annotations about the code that implements koment.

internal/mcp/writes.go

1 package mcp
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7
8 sdk "github.com/modelcontextprotocol/go-sdk/mcp"
9
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 )
14
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 )
21
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 }
30
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 }
37
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 }
44
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 }
52
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 }
60
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 }
68
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 }
81
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 }
89
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 }
96
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 }
117
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 }
139
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 }
161
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 }
178
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 }
192
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 }
200
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 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close