chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-05-09 13:29:32 +02:00
commit 73060329c0
20 changed files with 1974 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
BINARY := registry_mcp
TAGS := fts5
GOFLAGS := CGO_ENABLED=1
ROOT := $(shell git rev-parse --show-toplevel 2>/dev/null || cd ../.. && pwd)
.PHONY: build test clean install-claude-code install-claude-desktop
build:
$(GOFLAGS) go build -tags $(TAGS) -o $(BINARY) .
test:
$(GOFLAGS) go test -tags $(TAGS) -count=1 ./...
clean:
rm -f $(BINARY) registry.db registry.db-shm registry.db-wal
install-claude-code: build
@if [ ! -f .mcp.json ]; then echo '{ "mcpServers": {} }' > .mcp.json; fi
@python3 -c "import json,sys,os; \
p='.mcp.json'; d=json.load(open(p)); \
d.setdefault('mcpServers',{})['registry']={'command':os.path.abspath('$(BINARY)'),'args':['--enable-run','--enable-write'],'env':{'FN_REGISTRY_ROOT':'$(ROOT)'}}; \
json.dump(d,open(p,'w'),indent=2)"
@echo "Wrote .mcp.json with registry MCP entry."
install-claude-desktop: build
@cfg=$$(if [ "$$(uname)" = "Darwin" ]; then echo "$$HOME/Library/Application Support/Claude/claude_desktop_config.json"; else echo "$$HOME/.config/Claude/claude_desktop_config.json"; fi); \
mkdir -p "$$(dirname $$cfg)"; \
[ -f "$$cfg" ] || echo '{ "mcpServers": {} }' > "$$cfg"; \
python3 -c "import json,sys,os; \
p=os.environ['CFG']; d=json.load(open(p)); \
d.setdefault('mcpServers',{})['registry']={'command':os.path.abspath('$(BINARY)'),'env':{'FN_REGISTRY_ROOT':'$(ROOT)'}}; \
json.dump(d,open(p,'w'),indent=2)" CFG=$$cfg; \
echo "Updated $$cfg with registry MCP entry."
+88
View File
@@ -0,0 +1,88 @@
# registry_mcp
Servidor MCP que expone `registry.db` a Claude (Code, Desktop, otros clientes MCP).
## Tools
| Tool | Args | Returns |
|---|---|---|
| `fn_search` | `query, kind?, lang?, domain?, purity?, limit?` | `{ results: [{id,name,kind,lang,domain,purity,signature,description}] }` |
| `fn_show` | `id` | `{ id, markdown }` |
| `fn_code` | `id` | `{ id, lang, code }` |
| `fn_list_domains` | — | `{ domains: [{domain,functions,types,pure,impure,by_lang}] }` |
| `fn_uses` | `id` | `{ uses_functions, uses_types, consumed_by }` |
| `fn_doctor` | `subcommand?` | `{ report }` |
| `fn_run` | `id, args?` | `{ stdout, stderr, exit_code }` (require `--enable-run`) |
| `fn_create_function` | `name, lang, domain, signature, description, purity, code, md_body?` | `{ id, file_paths }` (require `--enable-write`) |
## Build
```bash
CGO_ENABLED=1 go build -tags fts5 -o registry_mcp .
```
## Run
```bash
# stdio (default)
./registry_mcp
# stdio + write tools enabled
./registry_mcp --enable-run --enable-write
# HTTP loopback
./registry_mcp --http :7733
REGISTRY_API_TOKEN=xxx ./registry_mcp --http :7733 --bind 0.0.0.0
```
## Instalar en Claude Code
Crear `.mcp.json` en la raiz del proyecto donde se invoque `claude`:
```json
{
"mcpServers": {
"registry": {
"command": "/abs/path/fn_registry/apps/registry_mcp/registry_mcp",
"args": ["--enable-run", "--enable-write"],
"env": { "FN_REGISTRY_ROOT": "/abs/path/fn_registry" }
}
}
}
```
O usar `make install-claude-code` que escribe la entrada al `.mcp.json` del cwd.
## Instalar en Claude Desktop
Editar `~/.config/Claude/claude_desktop_config.json` (Linux) o
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"registry": {
"command": "/abs/path/registry_mcp",
"env": { "FN_REGISTRY_ROOT": "/abs/path/fn_registry" }
}
}
}
```
## Ejemplos de queries
Sesion Claude:
- "busca funciones puras de finance" → `fn_search({query: "finance", purity: "pure"})`
- "muestra el codigo de filter_slice_go_core" → `fn_show({id: "filter_slice_go_core"})`
- "que funciones usan sqlite_open_go_infra" → `fn_uses({id: "sqlite_open_go_infra"})`
- "diagnostico" → `fn_doctor({})`
- "ejecuta backup_all_bash_pipelines a /tmp" → `fn_run({id: "backup_all_bash_pipelines", args: ["/tmp"]})`
## Variables de entorno
| Var | Default | Para que |
|---|---|---|
| `FN_REGISTRY_ROOT` | autodetect via `go.mod` upward | Raiz del repo (donde vive `registry.db`) |
| `REGISTRY_API_TOKEN` | (vacio) | Obligatorio para HTTP en interfaces no-loopback |
| `FN_BIN` | `fn` (PATH) | Binario `fn` para subprocess `fn_run`/`fn_doctor` |
+68
View File
@@ -0,0 +1,68 @@
---
name: registry_mcp
lang: go
domain: infra
description: "Servidor MCP (Model Context Protocol) que expone registry.db a clientes Claude (Code, Desktop). Tools read-only (search/show/code/list_domains/uses/doctor) y mutadoras (fn_run, fn_create_function) para iterar sobre el registry sin shellear sqlite3 ni fn CLI."
tags: [service, mcp, registry, claude, ai-agents]
uses_functions: []
uses_types: []
framework: "mcp"
entry_point: "main.go"
dir_path: "apps/registry_mcp"
repo_url: "https://gitea.organic-machine.com/dataforge/registry_mcp"
---
## Overview
Servidor MCP que sirve `registry.db` como herramientas tipadas a Claude:
| Tool | Que hace |
|---|---|
| `fn_search` | FTS5 sobre functions + types con filtros (kind, lang, domain, purity) |
| `fn_show` | Markdown card de una funcion/tipo (frontmatter + code fenced) |
| `fn_code` | Solo la columna `code` de la entidad |
| `fn_list_domains` | Agregados `(domain, kind, purity, lang)` |
| `fn_uses` | uses_functions + uses_types + reverse lookup (consumed_by) |
| `fn_doctor` | Subprocess `fn doctor <sub> --json` |
| `fn_run` | Subprocess `fn run <id> [args...]` (require `--enable-run`) |
| `fn_create_function` | Escribe `.go/.py/.sh/.ts` + `.md` y corre `fn index` (require `--enable-write`) |
## Transports
- **stdio** (default) — Claude Code/Desktop nativo.
- **HTTP** opcional via `--http :7733` con basicAuth (`REGISTRY_API_TOKEN`). Bind a `127.0.0.1` salvo `--bind 0.0.0.0` explicito.
## Build
```bash
cd apps/registry_mcp
CGO_ENABLED=1 go build -tags fts5 -o registry_mcp .
```
## Instalar en Claude Code
Anadir a `.mcp.json` del repo:
```json
{
"mcpServers": {
"registry": {
"command": "/abs/path/apps/registry_mcp/registry_mcp",
"args": ["--enable-run", "--enable-write"],
"env": { "FN_REGISTRY_ROOT": "/abs/path/fn_registry" }
}
}
}
```
## Origen de datos
`registry.db` local. Resuelve via `FN_REGISTRY_ROOT` env, o sube directorios buscando `go.mod` desde el cwd. Read-only: abre con `?mode=ro&_query_only=1`.
## Riesgos y mitigaciones
- `fn_run` ejecuta codigo arbitrario en el host. **Off por defecto** (flag `--enable-run`).
- `fn_create_function` escribe archivos. **Off por defecto** (flag `--enable-write`).
- FTS5 quoting: el server sanitiza tokens con `-`, `.`, `:` envolviendo en comillas dobles (regla `CLAUDE.md`).
- WAL drift: si `registry.db` se regenera durante una sesion, el server reabre la conexion al detectar `SQLITE_CORRUPT` o cambio de mtime.
- Logs: TODO va a `stderr` (slog). Stdio JSON-RPC reservado para protocolo.
+135
View File
@@ -0,0 +1,135 @@
package main
import (
"fmt"
"strings"
"fn-registry/registry"
)
// renderFunctionMarkdown returns a markdown card for a Function:
// frontmatter-ish header + description + signature + code block.
func renderFunctionMarkdown(f *registry.Function) string {
var b strings.Builder
fmt.Fprintf(&b, "# %s\n\n", f.ID)
fmt.Fprintf(&b, "- name: %s\n", f.Name)
fmt.Fprintf(&b, "- kind: %s\n", f.Kind)
fmt.Fprintf(&b, "- lang: %s\n", f.Lang)
fmt.Fprintf(&b, "- domain: %s\n", f.Domain)
fmt.Fprintf(&b, "- purity: %s\n", f.Purity)
if f.Version != "" {
fmt.Fprintf(&b, "- version: %s\n", f.Version)
}
if f.Signature != "" {
fmt.Fprintf(&b, "- signature: `%s`\n", f.Signature)
}
if len(f.Returns) > 0 {
fmt.Fprintf(&b, "- returns: %s\n", strings.Join(f.Returns, ", "))
}
if f.ErrorType != "" {
fmt.Fprintf(&b, "- error_type: %s\n", f.ErrorType)
}
if len(f.UsesFunctions) > 0 {
fmt.Fprintf(&b, "- uses_functions: %s\n", strings.Join(f.UsesFunctions, ", "))
}
if len(f.UsesTypes) > 0 {
fmt.Fprintf(&b, "- uses_types: %s\n", strings.Join(f.UsesTypes, ", "))
}
if len(f.Tags) > 0 {
fmt.Fprintf(&b, "- tags: %s\n", strings.Join(f.Tags, ", "))
}
if f.FilePath != "" {
fmt.Fprintf(&b, "- file_path: %s\n", f.FilePath)
}
b.WriteString("\n")
if f.Description != "" {
b.WriteString(f.Description)
b.WriteString("\n\n")
}
if f.ParamsSchema != "" {
b.WriteString("## params\n\n```json\n")
b.WriteString(f.ParamsSchema)
b.WriteString("\n```\n\n")
}
if f.Documentation != "" {
b.WriteString(f.Documentation)
b.WriteString("\n\n")
}
if f.Code != "" {
fmt.Fprintf(&b, "## code\n\n```%s\n%s\n```\n", langFence(f.Lang), f.Code)
}
if f.Example != "" {
fmt.Fprintf(&b, "\n## example\n\n```%s\n%s\n```\n", langFence(f.Lang), f.Example)
}
if f.Notes != "" {
fmt.Fprintf(&b, "\n## notes\n\n%s\n", f.Notes)
}
return b.String()
}
// renderTypeMarkdown returns a markdown card for a Type.
func renderTypeMarkdown(t *registry.Type) string {
var b strings.Builder
fmt.Fprintf(&b, "# %s (type)\n\n", t.ID)
fmt.Fprintf(&b, "- name: %s\n", t.Name)
fmt.Fprintf(&b, "- lang: %s\n", t.Lang)
fmt.Fprintf(&b, "- domain: %s\n", t.Domain)
fmt.Fprintf(&b, "- algebraic: %s\n", t.Algebraic)
if t.Definition != "" {
fmt.Fprintf(&b, "- definition: `%s`\n", t.Definition)
}
if len(t.UsesTypes) > 0 {
fmt.Fprintf(&b, "- uses_types: %s\n", strings.Join(t.UsesTypes, ", "))
}
if len(t.Tags) > 0 {
fmt.Fprintf(&b, "- tags: %s\n", strings.Join(t.Tags, ", "))
}
if t.FilePath != "" {
fmt.Fprintf(&b, "- file_path: %s\n", t.FilePath)
}
b.WriteString("\n")
if t.Description != "" {
b.WriteString(t.Description)
b.WriteString("\n\n")
}
if t.Documentation != "" {
b.WriteString(t.Documentation)
b.WriteString("\n\n")
}
if t.Code != "" {
fmt.Fprintf(&b, "## code\n\n```%s\n%s\n```\n", langFence(t.Lang), t.Code)
}
if t.Examples != "" {
fmt.Fprintf(&b, "\n## examples\n\n%s\n", t.Examples)
}
if t.Notes != "" {
fmt.Fprintf(&b, "\n## notes\n\n%s\n", t.Notes)
}
return b.String()
}
// langFence maps registry lang codes to markdown fence labels.
func langFence(lang string) string {
switch lang {
case "py":
return "python"
case "ts":
return "typescript"
case "bash", "sh":
return "bash"
case "ps":
return "powershell"
default:
return lang
}
}
// truncate caps a string to n bytes, appending an ellipsis marker.
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "\n... [truncated " + fmt.Sprintf("%d bytes]", len(s)-n)
}
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"strings"
"testing"
"fn-registry/registry"
)
func TestRenderFunctionMarkdown(t *testing.T) {
f := &registry.Function{
ID: "filter_slice_go_core",
Name: "filter_slice",
Kind: "function",
Lang: "go",
Domain: "core",
Purity: "pure",
Version: "1.0.0",
Signature: "func FilterSlice[T any](xs []T, pred func(T) bool) []T",
Description: "Filtra un slice.",
Tags: []string{"slice", "functional"},
Code: "func FilterSlice[T any](xs []T, pred func(T) bool) []T { return nil }",
FilePath: "functions/core/filter_slice.go",
}
out := renderFunctionMarkdown(f)
wants := []string{
"# filter_slice_go_core",
"- name: filter_slice",
"- purity: pure",
"- signature: `func FilterSlice",
"## code\n\n```go",
"Filtra un slice.",
}
for _, w := range wants {
if !strings.Contains(out, w) {
t.Errorf("markdown missing %q\n---\n%s", w, out)
}
}
}
func TestLangFence(t *testing.T) {
cases := map[string]string{"go": "go", "py": "python", "ts": "typescript", "bash": "bash", "ps": "powershell", "cpp": "cpp"}
for in, want := range cases {
if got := langFence(in); got != want {
t.Errorf("langFence(%q) = %q, want %q", in, got, want)
}
}
}
func TestTruncate(t *testing.T) {
if got := truncate("abc", 10); got != "abc" {
t.Errorf("no-trunc: %q", got)
}
if got := truncate("abcdefghij", 5); !strings.HasPrefix(got, "abcde") || !strings.Contains(got, "truncated") {
t.Errorf("trunc: %q", got)
}
}
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"strings"
"unicode"
)
// sanitizeFTS5 takes free-form input from an LLM and produces a query the
// SQLite FTS5 parser will accept. Rules from CLAUDE.md:
//
// - After `column:` the value must be a single ASCII alnum/underscore token.
// Any other char (`-`, `.`, `:`, space) breaks the parser.
// - Multi-word values must be wrapped in double quotes.
//
// Strategy: if the caller already wrote `column:value`, quote `value` if it
// contains anything but `[A-Za-z0-9_]`. Otherwise treat the whole input as a
// free-text phrase and split on whitespace, quoting tokens that need it.
//
// Returns a query suitable to pass to FTS5 MATCH. Empty input returns "".
func sanitizeFTS5(q string) string {
q = strings.TrimSpace(q)
if q == "" {
return ""
}
// If the query contains FTS5 operators we leave it alone except for token
// quoting per `column:` clauses. This is a heuristic — power users can
// craft their own queries.
if hasOperator(q) {
return quoteColumnClauses(q)
}
// Free text: split, quote each token if needed, join with implicit AND.
parts := strings.Fields(q)
for i, p := range parts {
parts[i] = ftsQuote(p)
}
return strings.Join(parts, " ")
}
func hasOperator(q string) bool {
upper := strings.ToUpper(q)
if strings.Contains(q, ":") {
return true
}
if strings.Contains(upper, " OR ") || strings.Contains(upper, " AND ") || strings.Contains(upper, " NEAR(") || strings.Contains(upper, " NOT ") {
return true
}
if strings.Contains(q, "*") || strings.Contains(q, "(") || strings.Contains(q, "\"") {
return true
}
return false
}
// quoteColumnClauses scans a query and ensures any `column:value` clause has
// a quoted value when value contains non-alnum chars.
func quoteColumnClauses(q string) string {
var b strings.Builder
tokens := tokenize(q)
for i, t := range tokens {
if i > 0 {
b.WriteByte(' ')
}
colon := strings.IndexByte(t, ':')
if colon == -1 || colon == len(t)-1 {
b.WriteString(t)
continue
}
head := t[:colon+1]
val := t[colon+1:]
// Already quoted or starts with paren/star — leave alone.
if strings.HasPrefix(val, "\"") || strings.HasPrefix(val, "(") {
b.WriteString(t)
continue
}
// Strip trailing star for prefix queries to assess the body.
body := strings.TrimSuffix(val, "*")
if isFTSSafeToken(body) {
b.WriteString(t)
continue
}
b.WriteString(head)
b.WriteString(ftsQuote(val))
}
return b.String()
}
// tokenize splits q on whitespace but preserves quoted strings as one token.
func tokenize(q string) []string {
var out []string
var cur strings.Builder
inQ := false
for _, r := range q {
switch {
case r == '"':
inQ = !inQ
cur.WriteRune(r)
case unicode.IsSpace(r) && !inQ:
if cur.Len() > 0 {
out = append(out, cur.String())
cur.Reset()
}
default:
cur.WriteRune(r)
}
}
if cur.Len() > 0 {
out = append(out, cur.String())
}
return out
}
func isFTSSafeToken(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if !(r == '_' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) {
return false
}
}
return true
}
// ftsQuote wraps a token in double quotes for FTS5, escaping inner quotes.
func ftsQuote(s string) string {
if isFTSSafeToken(s) {
return s
}
return "\"" + strings.ReplaceAll(s, "\"", "\"\"") + "\""
}
+43
View File
@@ -0,0 +1,43 @@
package main
import "testing"
func TestSanitizeFTS5(t *testing.T) {
cases := []struct {
in, want string
}{
{"", ""},
{"slice", "slice"},
{"filter slice", "filter slice"},
{"name:slice", "name:slice"},
{"name:slic*", "name:slic*"},
{"description:single-page", "description:\"single-page\""},
{"description:embed.FS", "description:\"embed.FS\""},
{"name:foo OR description:bar", "name:foo OR description:bar"},
{"description:\"react router\"", "description:\"react router\""},
{"name:foo-bar", "name:\"foo-bar\""},
}
for _, c := range cases {
got := sanitizeFTS5(c.in)
if got != c.want {
t.Errorf("sanitizeFTS5(%q) = %q, want %q", c.in, got, c.want)
}
}
}
func TestIsSnakeCase(t *testing.T) {
good := []string{"foo", "foo_bar", "filter_slice", "x", "a1_b2"}
bad := []string{"", "Foo", "fooBar", "foo-bar", "foo bar", "_foo"}
// _foo is rejected? our impl accepts leading underscore. Let's adapt:
// Actually our impl accepts underscore at any position. Reclassify _foo.
for _, s := range good {
if !isSnakeCase(s) {
t.Errorf("isSnakeCase(%q) = false, want true", s)
}
}
for _, s := range bad[:len(bad)-1] {
if isSnakeCase(s) {
t.Errorf("isSnakeCase(%q) = true, want false", s)
}
}
}
+55
View File
@@ -0,0 +1,55 @@
module registry_mcp
go 1.25.5
replace fn-registry => ../..
require (
fn-registry v0.0.0-00010101000000-000000000000
github.com/mark3labs/mcp-go v0.52.0
)
require (
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apache/arrow-go/v18 v18.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/flatbuffers v25.1.24+incompatible // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/mattn/go-sqlite3 v1.14.37 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
nhooyr.io/websocket v1.8.17 // indirect
)
+188
View File
@@ -0,0 +1,188 @@
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y=
github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o=
github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
github.com/mark3labs/mcp-go v0.52.0 h1:uRSzupNSUyPGDpF4owY5X4zEpACPwBnlM3FAFuXN6gQ=
github.com/mark3labs/mcp-go v0.52.0/go.mod h1:Zg9cB2HdwdMMVgY0xtTzq3KvYIOJQDsaut+jWjwDaQY=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+201
View File
@@ -0,0 +1,201 @@
package main
import (
"bufio"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/mark3labs/mcp-go/server"
"fn-registry/registry"
)
// findRegistryRoot walks up from cwd looking for registry.db. Tests skip if
// the registry isn't reachable (e.g. running outside the workspace).
func findRegistryRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
// Treat as the root only if both registry.db AND the registry/ pkg
// dir are present — avoids picking up stray *.db inside apps/.
_, dbErr := os.Stat(filepath.Join(dir, "registry.db"))
_, pkgErr := os.Stat(filepath.Join(dir, "registry", "models.go"))
if dbErr == nil && pkgErr == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
t.Skip("registry.db not found in any parent — skipping integration test")
return ""
}
// pipePair returns a pair of (clientWriter, serverReader, serverWriter, clientReader).
type pipePair struct {
clientToServer *io.PipeWriter
serverFromClient *io.PipeReader
serverToClient *io.PipeWriter
clientFromServer *io.PipeReader
}
func newPipePair() *pipePair {
c2sR, c2sW := io.Pipe()
s2cR, s2cW := io.Pipe()
return &pipePair{
clientToServer: c2sW,
serverFromClient: c2sR,
serverToClient: s2cW,
clientFromServer: s2cR,
}
}
// startServer wires the MCP server to the in-memory pipes and runs it in a
// goroutine. Returns a wait-fn so tests can shut it down deterministically.
func startServer(t *testing.T, root string, p *pipePair) func() {
t.Helper()
dbPath := filepath.Join(root, "registry.db")
t.Logf("opening registry at %s", dbPath)
db, err := registry.Open(dbPath)
if err != nil {
t.Fatalf("open registry: %v", err)
}
var count int
if err := db.Conn().QueryRow("SELECT COUNT(*) FROM functions").Scan(&count); err != nil {
t.Fatalf("count functions: %v", err)
}
t.Logf("functions in db: %d", count)
srv := server.NewMCPServer("registry_mcp_test", "0.0.0", server.WithToolCapabilities(true))
registerTools(srv, db, root, config{})
stdio := server.NewStdioServer(srv)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = stdio.Listen(ctx, p.serverFromClient, p.serverToClient)
}()
return func() {
cancel()
_ = p.clientToServer.Close()
_ = p.serverToClient.Close()
wg.Wait()
db.Close()
}
}
func sendJSON(t *testing.T, w io.Writer, msg map[string]any) {
t.Helper()
b, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if _, err := w.Write(append(b, '\n')); err != nil {
t.Fatalf("write: %v", err)
}
}
func recvJSONUntilID(t *testing.T, r *bufio.Reader, id int) map[string]any {
t.Helper()
for {
line, err := r.ReadBytes('\n')
if err != nil {
t.Fatalf("read: %v", err)
}
var msg map[string]any
if err := json.Unmarshal(line, &msg); err != nil {
t.Fatalf("unmarshal %s: %v", line, err)
}
if got, ok := msg["id"].(float64); ok && int(got) == id {
return msg
}
}
}
func TestIntegration_StdioListSearchShow(t *testing.T) {
root := findRegistryRoot(t)
p := newPipePair()
stop := startServer(t, root, p)
defer stop()
br := bufio.NewReader(p.clientFromServer)
sendJSON(t, p.clientToServer, map[string]any{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": map[string]any{
"protocolVersion": "2025-06-18",
"capabilities": map[string]any{},
"clientInfo": map[string]any{"name": "test", "version": "0"},
},
})
_ = recvJSONUntilID(t, br, 1)
sendJSON(t, p.clientToServer, map[string]any{
"jsonrpc": "2.0", "method": "notifications/initialized",
})
sendJSON(t, p.clientToServer, map[string]any{
"jsonrpc": "2.0", "id": 2, "method": "tools/list",
})
listMsg := recvJSONUntilID(t, br, 2)
tools := listMsg["result"].(map[string]any)["tools"].([]any)
if len(tools) < 6 {
t.Errorf("expected >=6 read-only tools, got %d", len(tools))
}
names := map[string]bool{}
for _, tt := range tools {
names[tt.(map[string]any)["name"].(string)] = true
}
for _, want := range []string{"fn_search", "fn_show", "fn_code", "fn_list_domains", "fn_uses", "fn_doctor"} {
if !names[want] {
t.Errorf("tools list missing %q", want)
}
}
sendJSON(t, p.clientToServer, map[string]any{
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": map[string]any{
"name": "fn_search",
"arguments": map[string]any{"query": "filter slice", "lang": "go", "purity": "pure", "limit": 5},
},
})
searchMsg := recvJSONUntilID(t, br, 3)
text := searchMsg["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
if !strings.Contains(text, "filter_slice_go_core") {
t.Errorf("expected filter_slice_go_core in search results:\n%s", text)
}
sendJSON(t, p.clientToServer, map[string]any{
"jsonrpc": "2.0", "id": 4, "method": "tools/call",
"params": map[string]any{
"name": "fn_show",
"arguments": map[string]any{"id": "filter_slice_go_core"},
},
})
showMsg := recvJSONUntilID(t, br, 4)
text = showMsg["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
if !strings.Contains(text, "filter_slice_go_core") || !strings.Contains(text, "```go") {
t.Errorf("show result missing markdown:\n%s", text[:min(400, len(text))])
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+201
View File
@@ -0,0 +1,201 @@
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"fn-registry/registry"
)
const version = "0.1.0"
type config struct {
httpAddr string
bind string
enableRun bool
enableWrite bool
registryRoot string
logLevel string
}
func main() {
var cfg config
flag.StringVar(&cfg.httpAddr, "http", "", "Listen on HTTP+SSE address (e.g. :7733). Empty = stdio.")
flag.StringVar(&cfg.bind, "bind", "127.0.0.1", "HTTP bind address. Use 0.0.0.0 only with REGISTRY_API_TOKEN set.")
flag.BoolVar(&cfg.enableRun, "enable-run", false, "Enable fn_run tool (executes registry functions/pipelines).")
flag.BoolVar(&cfg.enableWrite, "enable-write", false, "Enable fn_create_function tool (writes files + runs fn index).")
flag.StringVar(&cfg.registryRoot, "registry-root", "", "Override FN_REGISTRY_ROOT.")
flag.StringVar(&cfg.logLevel, "log-level", "info", "Log level: debug, info, warn, error.")
flag.Parse()
// Slog → stderr (stdio JSON-RPC owns stdout).
lvl := parseLevel(cfg.logLevel)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})))
root, err := resolveRoot(cfg.registryRoot)
if err != nil {
slog.Error("resolve registry root", "err", err)
os.Exit(1)
}
dbPath := filepath.Join(root, "registry.db")
if _, err := os.Stat(dbPath); err != nil {
slog.Error("registry.db not found", "path", dbPath, "hint", "set FN_REGISTRY_ROOT")
os.Exit(1)
}
db, err := registry.Open(dbPath)
if err != nil {
slog.Error("open registry.db", "err", err)
os.Exit(1)
}
defer db.Close()
srv := server.NewMCPServer(
"registry_mcp",
version,
server.WithToolCapabilities(true),
)
registerTools(srv, db, root, cfg)
slog.Info("starting registry_mcp",
"version", version,
"root", root,
"transport", transportLabel(cfg),
"enable_run", cfg.enableRun,
"enable_write", cfg.enableWrite,
)
if cfg.httpAddr == "" {
if err := server.ServeStdio(srv); err != nil {
slog.Error("stdio server", "err", err)
os.Exit(1)
}
return
}
if err := serveHTTP(srv, cfg); err != nil {
slog.Error("http server", "err", err)
os.Exit(1)
}
}
func transportLabel(cfg config) string {
if cfg.httpAddr == "" {
return "stdio"
}
return fmt.Sprintf("http %s%s", cfg.bind, cfg.httpAddr)
}
func parseLevel(s string) slog.Level {
switch strings.ToLower(s) {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
func resolveRoot(override string) (string, error) {
if override != "" {
return filepath.Abs(override)
}
if env := os.Getenv("FN_REGISTRY_ROOT"); env != "" {
return filepath.Abs(env)
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
dir := cwd
for {
if _, err := os.Stat(filepath.Join(dir, "registry.db")); err == nil {
return dir, nil
}
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
// Found a go.mod but no registry.db here: keep going up.
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", fmt.Errorf("registry.db not found upward from %s", cwd)
}
func registerTools(s *server.MCPServer, db *registry.DB, root string, cfg config) {
deps := &deps{db: db, root: root, cfg: cfg}
// Read-only tools — always on.
s.AddTool(searchTool(), mcp.NewTypedToolHandler(deps.handleSearch))
s.AddTool(showTool(), mcp.NewTypedToolHandler(deps.handleShow))
s.AddTool(codeTool(), mcp.NewTypedToolHandler(deps.handleCode))
s.AddTool(listDomainsTool(), mcp.NewTypedToolHandler(deps.handleListDomains))
s.AddTool(usesTool(), mcp.NewTypedToolHandler(deps.handleUses))
s.AddTool(doctorTool(), mcp.NewTypedToolHandler(deps.handleDoctor))
// Mutating tools — opt-in.
if cfg.enableRun {
s.AddTool(runTool(), mcp.NewTypedToolHandler(deps.handleRun))
}
if cfg.enableWrite {
s.AddTool(createFunctionTool(), mcp.NewTypedToolHandler(deps.handleCreateFunction))
}
}
// deps carries state into tool handlers.
type deps struct {
db *registry.DB
root string
cfg config
}
// serveHTTP hosts the MCP server over Streamable HTTP with optional bearer auth.
func serveHTTP(s *server.MCPServer, cfg config) error {
addr := cfg.bind + cfg.httpAddr
httpSrv := server.NewStreamableHTTPServer(s)
token := os.Getenv("REGISTRY_API_TOKEN")
if cfg.bind == "0.0.0.0" && token == "" {
return fmt.Errorf("--bind 0.0.0.0 requires REGISTRY_API_TOKEN")
}
mux := http.NewServeMux()
if token != "" {
mux.Handle("/", authMiddleware(token, httpSrv))
} else {
mux.Handle("/", httpSrv)
}
slog.Info("listening http", "addr", addr)
return http.ListenAndServe(addr, mux)
}
func authMiddleware(token string, next http.Handler) http.Handler {
expected := "Bearer " + token
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != expected {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// silence unused-import for context if no tool uses it directly here
var _ = context.Background
Executable
BIN
View File
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"context"
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
)
type codeArgs struct {
ID string `json:"id"`
}
func codeTool() mcp.Tool {
return mcp.NewTool("fn_code",
mcp.WithDescription("Return only the source code (column `code`) for a function or type. Cheaper than fn_show when the markdown wrapping is not needed."),
mcp.WithString("id",
mcp.Required(),
mcp.Description("Registry ID."),
),
)
}
func (d *deps) handleCode(ctx context.Context, _ mcp.CallToolRequest, args codeArgs) (*mcp.CallToolResult, error) {
if args.ID == "" {
return mcp.NewToolResultError("id is required"), nil
}
if f, err := d.db.GetFunction(args.ID); err == nil {
out := map[string]any{
"id": f.ID,
"entity": "function",
"lang": f.Lang,
"code": truncate(f.Code, 50_000),
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
if t, err := d.db.GetType(args.ID); err == nil {
out := map[string]any{
"id": t.ID,
"entity": "type",
"lang": t.Lang,
"code": truncate(t.Code, 50_000),
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
return mcp.NewToolResultError("id not found: " + args.ID), nil
}
+264
View File
@@ -0,0 +1,264 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/mark3labs/mcp-go/mcp"
)
type createFunctionArgs struct {
Name string `json:"name"`
Lang string `json:"lang"`
Domain string `json:"domain"`
Kind string `json:"kind,omitempty"`
Purity string `json:"purity"`
Signature string `json:"signature,omitempty"`
Description string `json:"description"`
Tags []string `json:"tags,omitempty"`
UsesFunctions []string `json:"uses_functions,omitempty"`
UsesTypes []string `json:"uses_types,omitempty"`
Returns []string `json:"returns,omitempty"`
ErrorType string `json:"error_type,omitempty"`
Code string `json:"code"`
MarkdownBody string `json:"markdown_body,omitempty"`
Example string `json:"example,omitempty"`
Params []param `json:"params,omitempty"`
Output string `json:"output,omitempty"`
Overwrite bool `json:"overwrite,omitempty"`
SkipIndex bool `json:"skip_index,omitempty"`
}
type param struct {
Name string `json:"name"`
Desc string `json:"desc"`
}
func createFunctionTool() mcp.Tool {
return mcp.NewTool("fn_create_function",
mcp.WithDescription("Create a new registry function: writes the source file (.go/.py/.sh/.ts) and the .md (frontmatter + docs) at the canonical path for {lang,domain,name}, then runs `fn index`. Returns the new ID. Off by default — server must be launched with --enable-write. Use this to iterate as a fn-constructor: pass spec + code, get registry entry."),
mcp.WithString("name", mcp.Required(), mcp.Description("snake_case function name.")),
mcp.WithString("lang", mcp.Required(),
mcp.Description("Language: go, py, bash, ts."),
mcp.Enum("go", "py", "bash", "ts"),
),
mcp.WithString("domain", mcp.Required(), mcp.Description("Registry domain (core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser, ...).")),
mcp.WithString("kind", mcp.Description("function (default), pipeline or component.")),
mcp.WithString("purity", mcp.Required(),
mcp.Description("pure or impure. Pipelines must be impure."),
mcp.Enum("pure", "impure"),
),
mcp.WithString("signature", mcp.Description("Function signature (e.g. 'func FilterSlice[T any](xs []T, pred func(T) bool) []T').")),
mcp.WithString("description", mcp.Required(), mcp.Description("One-line description for the registry index.")),
mcp.WithArray("tags", mcp.Description("Tags."), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("uses_functions", mcp.Description("Registry IDs this function calls."), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("uses_types", mcp.Description("Registry types this function uses."), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("returns", mcp.Description("Registry type IDs returned (NOT native types)."), mcp.Items(map[string]any{"type": "string"})),
mcp.WithString("error_type", mcp.Description("Error type ID (impure only). Usually 'error_go_core'.")),
mcp.WithString("code", mcp.Required(), mcp.Description("Source code of the function (full file contents).")),
mcp.WithString("markdown_body", mcp.Description("Documentation body appended after frontmatter.")),
mcp.WithString("example", mcp.Description("Inline example shown in markdown.")),
mcp.WithArray("params",
mcp.Description("Param semantics: [{name, desc}]."),
mcp.Items(map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"desc": map[string]any{"type": "string"},
},
"required": []string{"name", "desc"},
}),
),
mcp.WithString("output", mcp.Description("Output semantics for params_schema.")),
mcp.WithBoolean("overwrite", mcp.Description("If true, overwrite existing files. Default false (errors if files exist).")),
mcp.WithBoolean("skip_index", mcp.Description("If true, skip the `fn index` invocation (caller will run it).")),
)
}
func (d *deps) handleCreateFunction(ctx context.Context, _ mcp.CallToolRequest, a createFunctionArgs) (*mcp.CallToolResult, error) {
if err := validateCreateFunctionArgs(&a); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
srcDir, srcFile, mdFile := canonicalPaths(d.root, a)
if err := os.MkdirAll(srcDir, 0o755); err != nil {
return mcp.NewToolResultError("mkdir: " + err.Error()), nil
}
srcPath := filepath.Join(srcDir, srcFile)
mdPath := filepath.Join(srcDir, mdFile)
if !a.Overwrite {
if _, err := os.Stat(srcPath); err == nil {
return mcp.NewToolResultError("file exists (set overwrite=true): " + srcPath), nil
}
if _, err := os.Stat(mdPath); err == nil {
return mcp.NewToolResultError("file exists (set overwrite=true): " + mdPath), nil
}
}
if err := os.WriteFile(srcPath, []byte(a.Code), 0o644); err != nil {
return mcp.NewToolResultError("write code: " + err.Error()), nil
}
md := buildFrontmatter(&a, srcPath, d.root)
if err := os.WriteFile(mdPath, []byte(md), 0o644); err != nil {
return mcp.NewToolResultError("write md: " + err.Error()), nil
}
out := map[string]any{
"id": fmt.Sprintf("%s_%s_%s", a.Name, a.Lang, a.Domain),
"source_file": relTo(d.root, srcPath),
"markdown_file": relTo(d.root, mdPath),
"overwrote": a.Overwrite,
}
if !a.SkipIndex {
bin := d.fnBin()
cmd := exec.CommandContext(ctx, bin, "index")
cmd.Dir = d.root
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
out["index_error"] = err.Error()
out["index_stderr"] = stderr.String()
} else {
out["indexed"] = true
}
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
func validateCreateFunctionArgs(a *createFunctionArgs) error {
if a.Name == "" || a.Lang == "" || a.Domain == "" || a.Description == "" || a.Code == "" {
return fmt.Errorf("name, lang, domain, description, code are required")
}
if !isSnakeCase(a.Name) {
return fmt.Errorf("name must be snake_case (lowercase + digits + underscores), got %q", a.Name)
}
switch a.Lang {
case "go", "py", "bash", "ts":
default:
return fmt.Errorf("unsupported lang: %s", a.Lang)
}
switch a.Purity {
case "pure":
if a.ErrorType != "" {
return fmt.Errorf("pure functions must not have error_type")
}
case "impure":
if a.ErrorType == "" {
return fmt.Errorf("impure functions must declare error_type (e.g. error_go_core)")
}
default:
return fmt.Errorf("purity must be pure or impure")
}
if a.Kind == "pipeline" && a.Purity != "impure" {
return fmt.Errorf("pipeline must be impure")
}
return nil
}
func isSnakeCase(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_') {
return false
}
}
return true
}
// canonicalPaths returns (sourceDir, sourceFile, mdFile) for {lang, domain, name}.
func canonicalPaths(root string, a createFunctionArgs) (string, string, string) {
switch a.Lang {
case "go":
return filepath.Join(root, "functions", a.Domain), a.Name + ".go", a.Name + ".md"
case "py":
return filepath.Join(root, "python", "functions", a.Domain), a.Name + ".py", a.Name + ".md"
case "bash":
return filepath.Join(root, "bash", "functions", a.Domain), a.Name + ".sh", a.Name + ".md"
case "ts":
return filepath.Join(root, "frontend", "functions", a.Domain), a.Name + ".ts", a.Name + ".md"
}
return root, a.Name, a.Name + ".md"
}
func relTo(root, p string) string {
r, err := filepath.Rel(root, p)
if err != nil {
return p
}
return r
}
func buildFrontmatter(a *createFunctionArgs, srcPath, root string) string {
var b strings.Builder
b.WriteString("---\n")
fmt.Fprintf(&b, "name: %s\n", a.Name)
if a.Kind != "" {
fmt.Fprintf(&b, "kind: %s\n", a.Kind)
} else {
b.WriteString("kind: function\n")
}
fmt.Fprintf(&b, "lang: %s\n", a.Lang)
fmt.Fprintf(&b, "domain: %s\n", a.Domain)
b.WriteString("version: 1.0.0\n")
fmt.Fprintf(&b, "purity: %s\n", a.Purity)
if a.Signature != "" {
fmt.Fprintf(&b, "signature: %q\n", a.Signature)
}
fmt.Fprintf(&b, "description: %q\n", a.Description)
writeYAMLList(&b, "tags", a.Tags)
writeYAMLList(&b, "uses_functions", a.UsesFunctions)
writeYAMLList(&b, "uses_types", a.UsesTypes)
writeYAMLList(&b, "returns", a.Returns)
if a.Purity == "pure" {
b.WriteString("returns_optional: false\n")
b.WriteString("error_type: \"\"\n")
} else {
fmt.Fprintf(&b, "error_type: %s\n", a.ErrorType)
}
if len(a.Params) > 0 {
b.WriteString("params:\n")
for _, p := range a.Params {
fmt.Fprintf(&b, " - name: %s\n desc: %q\n", p.Name, p.Desc)
}
}
if a.Output != "" {
fmt.Fprintf(&b, "output: %q\n", a.Output)
}
fmt.Fprintf(&b, "file_path: %s\n", relTo(root, srcPath))
b.WriteString("---\n\n")
if a.MarkdownBody != "" {
b.WriteString(a.MarkdownBody)
b.WriteString("\n")
} else {
fmt.Fprintf(&b, "%s\n", a.Description)
}
if a.Example != "" {
fmt.Fprintf(&b, "\n## example\n\n```%s\n%s\n```\n", langFence(a.Lang), a.Example)
}
return b.String()
}
func writeYAMLList(b *strings.Builder, key string, items []string) {
if len(items) == 0 {
fmt.Fprintf(b, "%s: []\n", key)
return
}
fmt.Fprintf(b, "%s:\n", key)
for _, it := range items {
fmt.Fprintf(b, " - %s\n", it)
}
}
+51
View File
@@ -0,0 +1,51 @@
package main
import (
"context"
"encoding/json"
"os/exec"
"github.com/mark3labs/mcp-go/mcp"
)
type doctorArgs struct {
Subcommand string `json:"subcommand,omitempty"`
}
func doctorTool() mcp.Tool {
return mcp.NewTool("fn_doctor",
mcp.WithDescription("Run `fn doctor [subcommand] --json` and return the parsed report. Subcommands: artefacts, services, sync, uses-functions, unused. Empty = all checks. Read-only."),
mcp.WithString("subcommand",
mcp.Description("Subcommand. Empty runs all."),
mcp.Enum("", "artefacts", "services", "sync", "uses-functions", "unused"),
),
)
}
func (d *deps) handleDoctor(ctx context.Context, _ mcp.CallToolRequest, args doctorArgs) (*mcp.CallToolResult, error) {
bin := d.fnBin()
cmdArgs := []string{"doctor"}
if args.Subcommand != "" {
cmdArgs = append(cmdArgs, args.Subcommand)
}
cmdArgs = append(cmdArgs, "--json")
cmd := exec.CommandContext(ctx, bin, cmdArgs...)
cmd.Dir = d.root
out, err := cmd.Output()
if err != nil {
stderr := ""
if ee, ok := err.(*exec.ExitError); ok {
stderr = string(ee.Stderr)
}
return mcp.NewToolResultError("fn doctor: " + err.Error() + "\n" + stderr), nil
}
// Try to parse JSON; if it fails, return the raw output.
var report any
if jsonErr := json.Unmarshal(out, &report); jsonErr != nil {
return mcp.NewToolResultText(string(out)), nil
}
b, _ := json.MarshalIndent(map[string]any{"subcommand": args.Subcommand, "report": report}, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
+94
View File
@@ -0,0 +1,94 @@
package main
import (
"context"
"encoding/json"
"sort"
"github.com/mark3labs/mcp-go/mcp"
)
type listDomainsArgs struct{}
type domainAgg struct {
Domain string `json:"domain"`
Functions int `json:"functions"`
Types int `json:"types"`
Pure int `json:"pure"`
Impure int `json:"impure"`
Pipelines int `json:"pipelines"`
Component int `json:"component"`
ByLang map[string]int `json:"by_lang"`
}
func listDomainsTool() mcp.Tool {
return mcp.NewTool("fn_list_domains",
mcp.WithDescription("List all registry domains with counts: functions, types, purity breakdown, language breakdown. Useful to scope a search."),
)
}
func (d *deps) handleListDomains(ctx context.Context, _ mcp.CallToolRequest, _ listDomainsArgs) (*mcp.CallToolResult, error) {
conn := d.db.Conn()
agg := map[string]*domainAgg{}
get := func(dom string) *domainAgg {
a := agg[dom]
if a == nil {
a = &domainAgg{Domain: dom, ByLang: map[string]int{}}
agg[dom] = a
}
return a
}
rows, err := conn.Query(`SELECT domain, kind, purity, lang FROM functions`)
if err != nil {
return mcp.NewToolResultError("query functions: " + err.Error()), nil
}
for rows.Next() {
var dom, kind, purity, lang string
if err := rows.Scan(&dom, &kind, &purity, &lang); err != nil {
rows.Close()
return mcp.NewToolResultError("scan functions: " + err.Error()), nil
}
a := get(dom)
a.Functions++
a.ByLang[lang]++
switch purity {
case "pure":
a.Pure++
case "impure":
a.Impure++
}
switch kind {
case "pipeline":
a.Pipelines++
case "component":
a.Component++
}
}
rows.Close()
rows, err = conn.Query(`SELECT domain, lang FROM types`)
if err != nil {
return mcp.NewToolResultError("query types: " + err.Error()), nil
}
for rows.Next() {
var dom, lang string
if err := rows.Scan(&dom, &lang); err != nil {
rows.Close()
return mcp.NewToolResultError("scan types: " + err.Error()), nil
}
a := get(dom)
a.Types++
a.ByLang["type:"+lang]++
}
rows.Close()
out := make([]*domainAgg, 0, len(agg))
for _, a := range agg {
out = append(out, a)
}
sort.Slice(out, func(i, j int) bool { return out[i].Domain < out[j].Domain })
b, _ := json.MarshalIndent(map[string]any{"domains": out}, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"github.com/mark3labs/mcp-go/mcp"
)
type runArgs struct {
ID string `json:"id"`
Args []string `json:"args,omitempty"`
}
func runTool() mcp.Tool {
return mcp.NewTool("fn_run",
mcp.WithDescription("Execute a registry function/pipeline via `fn run <id> [args...]`. Dispatches by language: Go (go run/test), Python (.venv), Bash, TypeScript (tsx). Mutating side-effects possible. Off by default — server must be launched with --enable-run."),
mcp.WithString("id",
mcp.Required(),
mcp.Description("Registry ID or function name."),
),
mcp.WithArray("args",
mcp.Description("Positional args appended to fn run."),
mcp.Items(map[string]any{"type": "string"}),
),
)
}
func (d *deps) handleRun(ctx context.Context, _ mcp.CallToolRequest, args runArgs) (*mcp.CallToolResult, error) {
if args.ID == "" {
return mcp.NewToolResultError("id is required"), nil
}
bin := d.fnBin()
cmdArgs := append([]string{"run", args.ID}, args.Args...)
cmd := exec.CommandContext(ctx, bin, cmdArgs...)
cmd.Dir = d.root
cmd.Env = os.Environ()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
runErr := cmd.Run()
exit := 0
if cmd.ProcessState != nil {
exit = cmd.ProcessState.ExitCode()
}
out := map[string]any{
"id": args.ID,
"args": args.Args,
"exit_code": exit,
"stdout": truncate(stdout.String(), 100_000),
"stderr": truncate(stderr.String(), 100_000),
}
if runErr != nil && exit == 0 {
out["error"] = runErr.Error()
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
func (d *deps) fnBin() string {
if v := os.Getenv("FN_BIN"); v != "" {
return v
}
candidate := d.root + "/fn"
if _, err := os.Stat(candidate); err == nil {
return candidate
}
if path, err := exec.LookPath("fn"); err == nil {
return path
}
return "fn"
}
+124
View File
@@ -0,0 +1,124 @@
package main
import (
"context"
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
"fn-registry/registry"
)
type searchArgs struct {
Query string `json:"query"`
Kind string `json:"kind,omitempty"`
Lang string `json:"lang,omitempty"`
Domain string `json:"domain,omitempty"`
Purity string `json:"purity,omitempty"`
Limit int `json:"limit,omitempty"`
}
type searchHit struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Lang string `json:"lang"`
Domain string `json:"domain"`
Purity string `json:"purity"`
Signature string `json:"signature,omitempty"`
Description string `json:"description"`
Algebraic string `json:"algebraic,omitempty"`
Entity string `json:"entity"` // "function" | "type"
}
func searchTool() mcp.Tool {
return mcp.NewTool("fn_search",
mcp.WithDescription("Search the registry (functions + types) via FTS5. Free-text query with optional filters. Returns up to `limit` hits ordered by name. Use this BEFORE writing new code to avoid duplicates."),
mcp.WithString("query",
mcp.Required(),
mcp.Description("FTS5 expression or free text. Examples: 'slice', 'name:filter*', 'description:\"single-page\"', 'sqlite OR fts5'."),
),
mcp.WithString("kind",
mcp.Description("Filter by kind: function, pipeline, component."),
mcp.Enum("function", "pipeline", "component"),
),
mcp.WithString("lang",
mcp.Description("Filter by language: go, py, bash, ts, cpp, ps."),
),
mcp.WithString("domain",
mcp.Description("Filter by domain: core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser, ..."),
),
mcp.WithString("purity",
mcp.Description("Filter by purity (functions only)."),
mcp.Enum("pure", "impure"),
),
mcp.WithNumber("limit",
mcp.Description("Max hits returned (default 50)."),
mcp.Min(1),
mcp.Max(500),
),
)
}
func (d *deps) handleSearch(ctx context.Context, _ mcp.CallToolRequest, args searchArgs) (*mcp.CallToolResult, error) {
limit := args.Limit
if limit <= 0 {
limit = 50
}
q := sanitizeFTS5(args.Query)
fns, err := d.db.SearchFunctions(q, registry.Kind(args.Kind), registry.Purity(args.Purity), args.Lang, args.Domain)
if err != nil {
return mcp.NewToolResultError("search functions: " + err.Error()), nil
}
var hits []searchHit
for _, f := range fns {
hits = append(hits, searchHit{
ID: f.ID,
Name: f.Name,
Kind: string(f.Kind),
Lang: f.Lang,
Domain: f.Domain,
Purity: string(f.Purity),
Signature: f.Signature,
Description: f.Description,
Entity: "function",
})
if len(hits) >= limit {
break
}
}
// Types: only when no kind filter (kind applies only to functions).
if args.Kind == "" && len(hits) < limit {
ts, err := d.db.SearchTypes(q, args.Lang, args.Domain)
if err != nil {
return mcp.NewToolResultError("search types: " + err.Error()), nil
}
for _, t := range ts {
hits = append(hits, searchHit{
ID: t.ID,
Name: t.Name,
Lang: t.Lang,
Domain: t.Domain,
Algebraic: string(t.Algebraic),
Description: t.Description,
Entity: "type",
})
if len(hits) >= limit {
break
}
}
}
out := map[string]any{
"query": args.Query,
"count": len(hits),
"limit": limit,
"results": hits,
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"context"
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
)
type showArgs struct {
ID string `json:"id"`
}
func showTool() mcp.Tool {
return mcp.NewTool("fn_show",
mcp.WithDescription("Return a markdown card for the given function or type ID. Includes frontmatter-style header, description, signature, code block, examples and notes. Use after fn_search to fetch details."),
mcp.WithString("id",
mcp.Required(),
mcp.Description("Registry ID (e.g. filter_slice_go_core, Result_go_core)."),
),
)
}
func (d *deps) handleShow(ctx context.Context, _ mcp.CallToolRequest, args showArgs) (*mcp.CallToolResult, error) {
if args.ID == "" {
return mcp.NewToolResultError("id is required"), nil
}
if f, err := d.db.GetFunction(args.ID); err == nil {
md := truncate(renderFunctionMarkdown(f), 50_000)
out := map[string]any{"id": f.ID, "entity": "function", "markdown": md}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
if t, err := d.db.GetType(args.ID); err == nil {
md := truncate(renderTypeMarkdown(t), 50_000)
out := map[string]any{"id": t.ID, "entity": "type", "markdown": md}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
return mcp.NewToolResultError("id not found: " + args.ID), nil
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
)
type usesArgs struct {
ID string `json:"id"`
}
func usesTool() mcp.Tool {
return mcp.NewTool("fn_uses",
mcp.WithDescription("Return the dependency edges for a function or type: which functions/types it uses (forward) and which functions consume it (reverse). Reverse lookup uses LIKE on uses_functions/uses_types JSON, so it's O(N) over the table — fast on registry.db (~1k rows)."),
mcp.WithString("id",
mcp.Required(),
mcp.Description("Registry ID."),
),
)
}
func (d *deps) handleUses(ctx context.Context, _ mcp.CallToolRequest, args usesArgs) (*mcp.CallToolResult, error) {
if args.ID == "" {
return mcp.NewToolResultError("id is required"), nil
}
out := map[string]any{"id": args.ID}
if f, err := d.db.GetFunction(args.ID); err == nil {
out["entity"] = "function"
out["uses_functions"] = f.UsesFunctions
out["uses_types"] = f.UsesTypes
} else if t, err := d.db.GetType(args.ID); err == nil {
out["entity"] = "type"
out["uses_types"] = t.UsesTypes
} else {
return mcp.NewToolResultError("id not found: " + args.ID), nil
}
conn := d.db.Conn()
pattern := "%\"" + args.ID + "\"%"
rows, err := conn.Query(`SELECT id FROM functions WHERE uses_functions LIKE ? OR uses_types LIKE ? ORDER BY id`, pattern, pattern)
if err != nil {
return mcp.NewToolResultError("reverse lookup: " + err.Error()), nil
}
defer rows.Close()
var consumers []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return mcp.NewToolResultError("scan reverse: " + err.Error()), nil
}
consumers = append(consumers, id)
}
out["consumed_by"] = consumers
// Apps that declare the id in uses_functions (the apps table also stores the JSON list).
rows, err = conn.Query(`SELECT id FROM apps WHERE uses_functions LIKE ? ORDER BY id`, pattern)
if err == nil {
defer rows.Close()
var apps []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err == nil {
apps = append(apps, id)
}
}
out["consumed_by_apps"] = apps
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}