Files
wehub-resource-sync 4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:24 +08:00

241 lines
6.6 KiB
Go

package mcp
import (
"regexp"
"strings"
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/pkg/errors"
)
var curatedOperationIDs = []string{
"MemoService_ListMemos",
"MemoService_CreateMemo",
"MemoService_GetMemo",
"MemoService_UpdateMemo",
"MemoService_DeleteMemo",
"MemoService_ListMemoComments",
"MemoService_CreateMemoComment",
"MemoService_ListMemoAttachments",
"MemoService_SetMemoAttachments",
"MemoService_ListMemoReactions",
"MemoService_UpsertMemoReaction",
"MemoService_DeleteMemoReaction",
"MemoService_ListMemoRelations",
"MemoService_SetMemoRelations",
"AttachmentService_ListAttachments",
"AttachmentService_CreateAttachment",
"AttachmentService_GetAttachment",
"AttachmentService_DeleteAttachment",
"ShortcutService_ListShortcuts",
// The only allowed auth/identity operation: a read-only "whoami" so agents
// can resolve the current user (e.g. for ShortcutService_ListShortcuts).
"AuthService_GetCurrentUser",
}
type registeredOperation struct {
ToolName string
OperationID string
Method string
Path string
Operation *openAPIOperation
InputSchema jsonSchema
}
var wordBoundary = regexp.MustCompile(`([a-z0-9])([A-Z])`)
func buildCuratedTools(registry map[string]*openAPIOperation) ([]*sdkmcp.Tool, map[string]*registeredOperation, error) {
tools := make([]*sdkmcp.Tool, 0, len(curatedOperationIDs))
operations := map[string]*registeredOperation{}
for _, operationID := range curatedOperationIDs {
operation, ok := registry[operationID]
if !ok {
return nil, nil, errors.Errorf("curated OpenAPI operation %q not found", operationID)
}
tool, registered := buildToolFromOperation(operation)
if _, exists := operations[tool.Name]; exists {
return nil, nil, errors.Errorf("duplicate MCP tool name %q", tool.Name)
}
tools = append(tools, tool)
operations[tool.Name] = registered
}
return tools, operations, nil
}
func buildToolFromOperation(operation *openAPIOperation) (*sdkmcp.Tool, *registeredOperation) {
name := toolNameFromOperationID(operation.OperationID)
title := titleFromToolName(name)
inputSchema := inputSchemaForOperation(operation)
tool := &sdkmcp.Tool{
Meta: sdkmcp.Meta{
"operationId": operation.OperationID,
"method": operation.Method,
"path": operation.Path,
},
Name: name,
Title: title,
Description: operation.Description,
InputSchema: inputSchema,
OutputSchema: outputSchemaForOperation(operation),
Annotations: annotationsForOperation(operation, title),
}
return tool, &registeredOperation{
ToolName: name,
OperationID: operation.OperationID,
Method: operation.Method,
Path: operation.Path,
Operation: operation,
InputSchema: inputSchema,
}
}
func toolNameFromOperationID(operationID string) string {
service, method, ok := strings.Cut(operationID, "_")
if !ok {
return camelToSnake(operationID)
}
service = strings.TrimSuffix(service, "Service")
return camelToSnake(service) + "_" + camelToSnake(method)
}
func camelToSnake(value string) string {
return strings.ToLower(wordBoundary.ReplaceAllString(value, `${1}_${2}`))
}
func titleFromToolName(name string) string {
parts := strings.Split(name, "_")
for i, part := range parts {
if part == "" {
continue
}
parts[i] = strings.ToUpper(part[:1]) + part[1:]
}
return strings.Join(parts, " ")
}
func inputSchemaForOperation(operation *openAPIOperation) jsonSchema {
properties := map[string]any{}
required := []string{}
defs := map[string]any{}
for _, parameter := range operation.Parameters {
schema := cloneSchema(parameter.Schema)
if parameter.Description != "" {
schema["description"] = parameter.Description
}
properties[parameter.Name] = schema
if parameter.Required {
required = append(required, parameter.Name)
}
}
if operation.RequestBody != nil {
bodySchema := requestBodySchema(operation)
for name, definition := range extractSchemaDefs(bodySchema) {
defs[name] = definition
}
properties["body"] = bodySchema
if operation.RequestBody.Required {
required = append(required, "body")
}
}
schema := jsonSchema{
"type": "object",
"properties": properties,
"additionalProperties": false,
}
if len(required) > 0 {
schema["required"] = required
}
if len(defs) > 0 {
schema["$defs"] = defs
}
return schema
}
func requestBodySchema(operation *openAPIOperation) jsonSchema {
if operation.RequestBodySchema == nil {
return jsonSchema{"type": "object"}
}
return cloneSchema(operation.RequestBodySchema)
}
func outputSchemaForOperation(operation *openAPIOperation) jsonSchema {
if operation.ResponseSchema == nil {
return okSchema()
}
return cloneSchema(operation.ResponseSchema)
}
func cloneSchema(schema jsonSchema) jsonSchema {
clone := jsonSchema{}
for key, value := range schema {
clone[key] = value
}
return clone
}
func extractSchemaDefs(schema jsonSchema) map[string]any {
defs, ok := schema["$defs"].(map[string]any)
if !ok {
return nil
}
delete(schema, "$defs")
return defs
}
// idempotentOperationIDs lists operations whose idempotency the HTTP-method
// heuristic gets wrong. The "Set*" operations declaratively replace the full
// set on a memo, so repeating an identical call converges to the same state —
// idempotent — even though they are served over PATCH (which the heuristic
// treats as non-idempotent).
var idempotentOperationIDs = map[string]bool{
"MemoService_SetMemoAttachments": true,
"MemoService_SetMemoRelations": true,
}
// annotationsForOperation derives the method-based annotations and then applies
// per-operation overrides that the HTTP method alone cannot express.
func annotationsForOperation(operation *openAPIOperation, title string) *sdkmcp.ToolAnnotations {
annotations := annotationsForMethod(operation.Method, title)
if idempotentOperationIDs[operation.OperationID] {
annotations.IdempotentHint = true
}
return annotations
}
func annotationsForMethod(method string, title string) *sdkmcp.ToolAnnotations {
openWorld := false
destructive := false
switch strings.ToUpper(method) {
case "GET":
return &sdkmcp.ToolAnnotations{
Title: title,
ReadOnlyHint: true,
DestructiveHint: &destructive,
IdempotentHint: true,
OpenWorldHint: &openWorld,
}
case "DELETE":
destructive = true
return &sdkmcp.ToolAnnotations{
Title: title,
ReadOnlyHint: false,
DestructiveHint: &destructive,
IdempotentHint: true,
OpenWorldHint: &openWorld,
}
default:
return &sdkmcp.ToolAnnotations{
Title: title,
ReadOnlyHint: false,
DestructiveHint: &destructive,
IdempotentHint: false,
OpenWorldHint: &openWorld,
}
}
}