Files
agentic-coding-workflow/internal/mcp/tools_memory.go
T
2026-06-22 08:55:57 +08:00

212 lines
6.9 KiB
Go

package mcp
import (
"context"
"github.com/google/uuid"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
"github.com/yan1h/agent-coding-workflow/internal/project"
"github.com/yan1h/agent-coding-workflow/internal/projectmemory"
)
// registerMemoryTools registers the project_memory tools. When the Memory service
// is nil the tools are not registered.
func registerMemoryTools(srv *mcpsdk.Server, deps ServerDeps) {
if deps.Memory == nil {
return
}
registerMemoryWrite(srv, deps)
registerMemorySearch(srv, deps)
registerMemoryList(srv, deps)
}
// resolveMemoryScope resolves (projectID, workspaceID?) from either a
// workspace_id or a project_slug, enforcing project scope. Exactly one of the two
// identifiers must be supplied for write/search; list requires project_slug.
func resolveMemoryScope(ctx context.Context, deps ServerDeps, c project.Caller, workspaceID, projectSlug string) (projectID uuid.UUID, wsID *uuid.UUID, err error) {
switch {
case workspaceID != "":
w, _, werr := scopedWorkspace(ctx, deps, c, workspaceID)
if werr != nil {
return uuid.Nil, nil, werr
}
id := w.ID
return w.ProjectID, &id, nil
case projectSlug != "":
p, perr := deps.Projects.Get(ctx, c, projectSlug)
if perr != nil {
return uuid.Nil, nil, perr
}
if cerr := CheckProjectFromCtx(ctx, p.ID); cerr != nil {
return uuid.Nil, nil, cerr
}
return p.ID, nil, nil
default:
return uuid.Nil, nil, errs.New(errs.CodeInvalidInput, "project_slug or workspace_id required")
}
}
type memoryEntryDTO struct {
ID string `json:"id"`
Kind string `json:"kind"`
Title string `json:"title"`
Body string `json:"body"`
Tags []string `json:"tags"`
Source string `json:"source"`
WorkspaceID string `json:"workspace_id,omitempty"`
CreatedAt string `json:"created_at"`
}
func memoryEntryDTOFrom(e projectmemory.Entry) memoryEntryDTO {
d := memoryEntryDTO{
ID: e.ID.String(), Kind: e.Kind, Title: e.Title, Body: e.Body,
Tags: e.Tags, Source: e.Source,
CreatedAt: e.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if e.WorkspaceID != nil {
d.WorkspaceID = e.WorkspaceID.String()
}
if d.Tags == nil {
d.Tags = []string{}
}
return d
}
// ===== memory_write =====
type memoryWriteIn struct {
ProjectSlug string `json:"project_slug,omitempty" jsonschema:"project slug (or supply workspace_id)"`
WorkspaceID string `json:"workspace_id,omitempty" jsonschema:"workspace UUID (scopes the entry to this workspace)"`
Kind string `json:"kind" jsonschema:"one of: decision, convention, file_map, gotcha"`
Title string `json:"title"`
Body string `json:"body"`
Tags []string `json:"tags,omitempty"`
}
type memoryWriteOut struct {
OK bool `json:"ok"`
ID string `json:"id"`
}
func registerMemoryWrite(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "memory_write", Description: "Record a typed project memory entry (decision/convention/file_map/gotcha)."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in memoryWriteIn) (*mcpsdk.CallToolResult, memoryWriteOut, error) {
if err := CheckToolFromCtx(ctx, "memory_write"); err != nil {
return nil, memoryWriteOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, memoryWriteOut{}, err
}
projectID, wsID, err := resolveMemoryScope(ctx, deps, c, in.WorkspaceID, in.ProjectSlug)
if err != nil {
return nil, memoryWriteOut{}, err
}
uid := c.UserID
entry, err := deps.Memory.Write(ctx, projectmemory.WriteInput{
ProjectID: projectID,
WorkspaceID: wsID,
Kind: in.Kind,
Title: in.Title,
Body: in.Body,
Tags: in.Tags,
Source: projectmemory.SourceAgent,
CreatedBy: &uid,
})
if err != nil {
return nil, memoryWriteOut{}, err
}
return nil, memoryWriteOut{OK: true, ID: entry.ID.String()}, nil
})
}
// ===== memory_search =====
type memorySearchIn struct {
ProjectSlug string `json:"project_slug,omitempty" jsonschema:"project slug (or supply workspace_id)"`
WorkspaceID string `json:"workspace_id,omitempty" jsonschema:"workspace UUID"`
Query string `json:"query" jsonschema:"search text"`
Kind string `json:"kind,omitempty" jsonschema:"restrict to a kind"`
TopK int `json:"top_k,omitempty"`
}
type memorySearchOut struct {
Results []memoryEntryDTO `json:"results"`
}
func registerMemorySearch(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "memory_search", Description: "Search project memory (vector when embeddings available, keyword/tag fallback otherwise)."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in memorySearchIn) (*mcpsdk.CallToolResult, memorySearchOut, error) {
if err := CheckToolFromCtx(ctx, "memory_search"); err != nil {
return nil, memorySearchOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, memorySearchOut{}, err
}
projectID, _, err := resolveMemoryScope(ctx, deps, c, in.WorkspaceID, in.ProjectSlug)
if err != nil {
return nil, memorySearchOut{}, err
}
entries, err := deps.Memory.Search(ctx, projectmemory.Query{
ProjectID: projectID, Text: in.Query, Kind: in.Kind, TopK: in.TopK,
})
if err != nil {
return nil, memorySearchOut{}, err
}
out := memorySearchOut{Results: make([]memoryEntryDTO, 0, len(entries))}
for _, e := range entries {
out.Results = append(out.Results, memoryEntryDTOFrom(e))
}
return nil, out, nil
})
}
// ===== memory_list =====
type memoryListIn struct {
ProjectSlug string `json:"project_slug" jsonschema:"project slug"`
Kind string `json:"kind,omitempty" jsonschema:"restrict to a kind"`
}
type memoryListOut struct {
Entries []memoryEntryDTO `json:"entries"`
}
func registerMemoryList(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "memory_list", Description: "List project memory entries, optionally filtered by kind."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in memoryListIn) (*mcpsdk.CallToolResult, memoryListOut, error) {
if err := CheckToolFromCtx(ctx, "memory_list"); err != nil {
return nil, memoryListOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, memoryListOut{}, err
}
if in.ProjectSlug == "" {
return nil, memoryListOut{}, errs.New(errs.CodeInvalidInput, "project_slug required")
}
p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
if err != nil {
return nil, memoryListOut{}, err
}
if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
return nil, memoryListOut{}, err
}
entries, err := deps.Memory.List(ctx, p.ID, in.Kind)
if err != nil {
return nil, memoryListOut{}, err
}
out := memoryListOut{Entries: make([]memoryEntryDTO, 0, len(entries))}
for _, e := range entries {
out.Entries = append(out.Entries, memoryEntryDTOFrom(e))
}
return nil, out, nil
})
}