MCP Server

Model Context Protocol (MCP) сервер для интеграции заметок с AI-агентами (Claude, Cursor, etc).

Endpoint

POST /_system/mcp
Content-Type: application/json
Accept: application/json, text/event-stream

JSON-RPC 2.0 протокол.

Транспорт

Транспорт — официальный Go MCP SDK (github.com/modelcontextprotocol/go-sdk),
Streamable HTTP в stateless-режиме с JSON-ответами. SDK владеет протоколом:
разбором JSON-RPC, диспетчеризацией, реестром инструментов и согласованием
версии протокола. Наш код в internal/case/mcp отвечает за аутентификацию,
метрики, лимит глубины федерации и сами инструменты.

Заголовок Accept обязателен и должен содержать оба типа —
application/json и text/event-stream. Запрос без него получает 400. Это
требование спецификации Streamable HTTP; настоящие MCP-клиенты его посылают,
но curl-примеры и собственные интеграции нужно обновить.

Точный формат ответов зафиксирован в
internal/case/mcp/testdata/endpoint_golden.txt. Перезаписать:

go test ./internal/case/mcp -run TestEndpointGolden -update-golden

Отличия от прежней самописной реализации

Запрос Было Стало
Без Accept работал 400
notifications/initialized 200 + {"result":{}} 202, пустое тело
initialize без protocolVersion всегда 2024-11-05 согласование → 2025-11-25
capabilities {"tools":{}} {"logging":{},"tools":{"listChanged":true}}
resources/list -32601 пустой список (SDK его реализует)
tools/list порядок объявления по имени, плюс поля ttlMs/cacheScope

Имена, описания и схемы инструментов не изменились — они и есть контракт для
агентов.

Tools

Базовые

Tool Описание Параметры
search Hybrid search по заметкам query: string
similar Похожие заметки (vector search) text: string, limit?: number

Динамические методы

Заметки с mcp_method в frontmatter становятся callable tools.

Frontmatter формат

---
free: true                              # обязательно для публичного доступа
mcp_method: code-review                 # имя метода (tool name)
mcp_description: "Детальный code review"  # описание для tools/list
---

Контент заметки - это то, что вернётся при вызове метода.
Может содержать любой текст, шаблоны, инструкции.

Примеры использования

tools/list

Request:

{
  "jsonrpc": "2.0",
  "method": "tools/list",
  "id": 1
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "search",
        "description": "Search notes by query",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": {"type": "string"}
          },
          "required": ["query"]
        }
      },
      {
        "name": "similar",
        "description": "Find similar notes by text",
        "inputSchema": {
          "type": "object",
          "properties": {
            "text": {"type": "string"},
            "limit": {"type": "number"}
          },
          "required": ["text"]
        }
      },
      {
        "name": "code-review",
        "description": "Детальный code review"
      }
    ]
  }
}

Request:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "query": "golang error handling"
    }
  },
  "id": 2
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Found 3 notes:\n\n1. Error Handling in Go\n   /programming/go-errors\n\n2. ..."
      }
    ]
  }
}

tools/call {method}

Request:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "code-review",
    "arguments": {}
  },
  "id": 3
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Контент заметки code-review..."
      }
    ]
  }
}

Фильтрация заметок

  • search/similar: все заметки с free: true
  • dynamic methods: заметки с free: true И mcp_method заданным

План имплементации

Story 1: Поля в NoteView

Добавить в internal/model/note.go:

type NoteView struct {
    // ... existing fields ...

    MCPMethod      string // from frontmatter mcp_method
    MCPDescription string // from frontmatter mcp_description
}

Добавить extraction в ExtractMetaData():

func (n *NoteView) extractMCPFields() {
    if method, ok := n.RawMeta["mcp_method"].(string); ok {
        n.MCPMethod = method
    }
    if desc, ok := n.RawMeta["mcp_description"].(string); ok {
        n.MCPDescription = desc
    }
}

Story 2: MCP Endpoint

Создать internal/case/mcp/:

internal/case/mcp/
├── endpoint.go      # HTTP handler, JSON-RPC routing
├── resolve.go       # main logic
├── tools.go         # tools/list implementation
├── search.go        # search tool
├── similar.go       # similar tool
└── method.go        # dynamic method dispatch

endpoint.go:

package mcp

type Endpoint struct{}

func (*Endpoint) Path() string {
    return "/_system/mcp"
}

func (*Endpoint) Method() string {
    return http.MethodPost
}

Story 3: tools/list

func (h *Handler) ListTools(env Env) ListToolsResult {
    tools := []Tool{
        {Name: "search", Description: "Search notes", InputSchema: searchSchema},
        {Name: "similar", Description: "Find similar notes", InputSchema: similarSchema},
    }

    // Add dynamic methods from notes
    for _, note := range env.LiveNoteViews().List {
        if note.Free && note.MCPMethod != "" {
            tools = append(tools, Tool{
                Name:        note.MCPMethod,
                Description: note.MCPDescription,
            })
        }
    }

    return ListToolsResult{Tools: tools}
}

Story 4: tools/call search

Переиспользовать internal/case/searchnotes/:

func (h *Handler) CallSearch(env Env, query string) CallToolResult {
    results := searchnotes.Resolve(ctx, env, searchnotes.Input{Query: query})

    // Filter to free notes only
    // Format as text response

    return CallToolResult{Content: [...]}
}

Story 5: tools/call similar

Переиспользовать существующий vector search:

func (h *Handler) CallSimilar(env Env, text string, limit int) CallToolResult {
    // Get embedding for text
    // Find similar notes by vector
    // Filter to free notes
    // Format response
}

Story 6: tools/call {method}

func (h *Handler) CallMethod(env Env, methodName string) CallToolResult {
    for _, note := range env.LiveNoteViews().List {
        if note.Free && note.MCPMethod == methodName {
            return CallToolResult{
                Content: []Content{{Type: "text", Text: string(note.Content)}},
            }
        }
    }

    return ErrorResult{Code: -32601, Message: "Method not found"}
}

Фаза 2 (будущее)

Marketplace для промптов:

  • Платные методы (price: 0.01 в frontmatter)
  • Авторизация пользователей
  • Трекинг usage для billing
  • Discovery: search_marketplace tool
  • Per-author endpoints: /_system/mcp/{username}