// Copyright 2026 printing-press-golden and contributors. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package mcp

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	mcplib "github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"mcp-cloudflare-pp-cli/internal/cli"
	"mcp-cloudflare-pp-cli/internal/client"
	"mcp-cloudflare-pp-cli/internal/cliutil"
	"mcp-cloudflare-pp-cli/internal/config"
	"mcp-cloudflare-pp-cli/internal/mcp/bound"
	"mcp-cloudflare-pp-cli/internal/mcp/cobratree"
	"mcp-cloudflare-pp-cli/internal/store"
)

const (
	// MCP hosts can fan out tool calls faster than a human CLI session.
	// Keep them on the same polite-client limiter path instead of disabling
	// pacing with rate=0; users can still tune human CLI calls with --rate-limit.
	defaultMCPRateLimit = 2
)

// RegisterTools registers all API operations as MCP tools.
func RegisterTools(s *server.MCPServer) {
	// Code-orchestration mode — the full surface is covered by two tools
	// (<api>_search + <api>_execute). Endpoint-mirror tools are suppressed.
	RegisterCodeOrchestrationTools(s)
	// Search tool — faster than iterating list endpoints for finding specific items
	s.AddTool(
		mcplib.NewTool("search",
			mcplib.WithDescription("Full-text search across all synced data. Faster than paginating list endpoints. Requires sync first."),
			mcplib.WithString("query", mcplib.Required(), mcplib.Description("Search query (supports FTS5 syntax: AND, OR, NOT, quotes for phrases)")),
			mcplib.WithNumber("limit", mcplib.Description("Max results (default 25)")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
		),
		handleSearch,
	)
	// SQL tool — ad-hoc analysis on synced data without API calls
	s.AddTool(
		mcplib.NewTool("sql",
			mcplib.WithDescription("Run read-only SQL against local database. Use for ad-hoc analysis, aggregations, and joins across synced resources. Requires sync first."),
			mcplib.WithString("query", mcplib.Required(), mcplib.Description("SQL query (SELECT or WITH...SELECT). Synced records live in resources(resource_type, id, data); filter by resource_type and use json_extract on data, e.g. SELECT json_extract(data,'$.name') FROM resources WHERE resource_type='items'.")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
		),
		handleSQL,
	)

	// Context tool — front-loaded domain knowledge for agents.
	// Call this first to understand the API taxonomy, query patterns, and capabilities.
	s.AddTool(
		mcplib.NewTool("context",
			mcplib.WithDescription("Get API domain context: resource taxonomy, auth requirements, query tips, and unique capabilities. Call this first."),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
		),
		handleContext,
	)

	// Runtime Cobra-tree mirror — exposes every user-facing command that is
	// not already covered by a typed endpoint or framework MCP tool.
	cobratree.RegisterAll(s, cli.RootCmd(), cobratree.SiblingCLIPath)
}

type mcpParamBinding struct {
	PublicName string
	WireName   string
	Location   string
}

type mcpPageConfig struct {
	CursorParam    string
	NextCursorPath string
}

func formatMCPParamValue(v any) string {
	switch tv := v.(type) {
	case string:
		return tv
	case bool:
		return strconv.FormatBool(tv)
	case float64:
		if math.IsNaN(tv) || math.IsInf(tv, 0) {
			return strconv.FormatFloat(tv, 'f', -1, 64)
		}
		if math.Trunc(tv) == tv && math.Abs(tv) < 1e15 {
			return strconv.FormatInt(int64(tv), 10)
		}
		return strconv.FormatFloat(tv, 'f', -1, 64)
	case float32:
		f := float64(tv)
		if math.IsNaN(f) || math.IsInf(f, 0) {
			return strconv.FormatFloat(f, 'f', -1, 32)
		}
		if math.Trunc(f) == f && math.Abs(f) < 1e15 {
			return strconv.FormatInt(int64(f), 10)
		}
		return strconv.FormatFloat(f, 'f', -1, 32)
	default:
		// Composite values (a native []any / map[string]any from an array or
		// object param) reach this path when bound to a query or path slot;
		// JSON-encode them so the wire value is valid JSON rather than Go's
		// "[a b c]" / "map[...]" rendering. Body params never come through
		// here — they are stored natively in bodyArgs and marshalled there.
		if b, err := json.Marshal(v); err == nil {
			return string(b)
		}
		return fmt.Sprintf("%v", v)
	}
}

// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
func makeAPIHandler(method, pathTemplate string, readOnly bool, binaryResponse bool, headerOverrides map[string]string, pageConfig mcpPageConfig, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
		c, err := newMCPClient()
		if err != nil {
			return mcplib.NewToolResultError(err.Error()), nil
		}

		// mcp-go v0.47+ made CallToolParams.Arguments an `any` to support
		// non-map payloads; GetArguments() returns the map[string]any shape
		// we rely on here (or an empty map when the payload is something else).
		args := req.GetArguments()

		// positionalParams mixes real URL path params with CLI positional
		// args that map to query params (e.g. `search <query>` -> ?query=);
		// the placeholder check below disambiguates them at runtime.
		path := pathTemplate
		knownArgs := make(map[string]bool, len(bindings))
		pathParams := make(map[string]bool, len(positionalParams))
		params := make(map[string]string)
		bodyArgs := make(map[string]any)
		mcpCursor := ""
		if pageConfig.CursorParam != "" {
			knownArgs["cursor"] = true
			if v, ok := args["cursor"]; ok {
				s, ok := v.(string)
				if !ok {
					return mcplib.NewToolResultError("cursor must be an opaque string returned by a previous MCP response"), nil
				}
				mcpCursor = s
				upstreamCursor, err := bound.UpstreamCursor(s)
				if err != nil {
					return mcplib.NewToolResultError(err.Error()), nil
				}
				if upstreamCursor != "" {
					params[pageConfig.CursorParam] = upstreamCursor
				}
			}
		}
		var headers map[string]string
		if len(headerOverrides) > 0 {
			headers = make(map[string]string, len(headerOverrides)+1)
			for k, v := range headerOverrides {
				headers[k] = v
			}
		}
		if binaryResponse {
			if headers == nil {
				headers = map[string]string{}
			}
			headers[client.BinaryResponseHeader] = "true"
		}
		for _, binding := range bindings {
			knownArgs[binding.PublicName] = true
			v, ok := args[binding.PublicName]
			if !ok {
				continue
			}
			switch binding.Location {
			case "path":
				placeholder := "{" + binding.WireName + "}"
				pathParams[binding.PublicName] = true
				path = strings.Replace(path, placeholder, formatMCPParamValue(v), 1)
			case "body":
				bodyArgs[binding.WireName] = v
			default:
				params[binding.WireName] = formatMCPParamValue(v)
			}
		}
		for _, p := range positionalParams {
			placeholder := "{" + p + "}"
			if !strings.Contains(pathTemplate, placeholder) {
				continue
			}
			pathParams[p] = true
			if v, ok := args[p]; ok {
				path = strings.Replace(path, placeholder, formatMCPParamValue(v), 1)
			}
		}

		for k, v := range args {
			if pathParams[k] || knownArgs[k] {
				continue
			}
			switch method {
			case "POST", "PUT", "PATCH":
				bodyArgs[k] = v
			default:
				params[k] = formatMCPParamValue(v)
			}
		}

		var data json.RawMessage
		switch method {
		case "GET":
			if len(headers) > 0 {
				data, err = c.GetWithHeaders(ctx, path, params, headers)
				break
			}
			data, err = c.Get(ctx, path, params)
		case "POST":
			if len(headers) > 0 {
				if readOnly {
					data, _, err = c.PostQueryWithParamsAndHeaders(ctx, path, params, bodyArgs, headers)
				} else {
					data, _, err = c.PostWithParamsAndHeaders(ctx, path, params, bodyArgs, headers)
				}
				break
			}
			if readOnly {
				data, _, err = c.PostQueryWithParams(ctx, path, params, bodyArgs)
			} else {
				data, _, err = c.PostWithParams(ctx, path, params, bodyArgs)
			}
		case "PUT":
			if len(headers) > 0 {
				data, _, err = c.PutWithParamsAndHeaders(ctx, path, params, bodyArgs, headers)
				break
			}
			data, _, err = c.PutWithParams(ctx, path, params, bodyArgs)
		case "PATCH":
			if len(headers) > 0 {
				data, _, err = c.PatchWithParamsAndHeaders(ctx, path, params, bodyArgs, headers)
				break
			}
			data, _, err = c.PatchWithParams(ctx, path, params, bodyArgs)
		case "DELETE":
			if len(headers) > 0 {
				data, _, err = c.DeleteWithParamsAndHeaders(ctx, path, params, headers)
				break
			}
			data, _, err = c.DeleteWithParams(ctx, path, params)
		default:
			return mcplib.NewToolResultError("unsupported method: " + method), nil
		}

		if err != nil {
			msg := err.Error()
			switch {
			case strings.Contains(msg, "HTTP 409"):
				return mcplib.NewToolResultText("already exists (no-op)"), nil
			case strings.Contains(msg, "HTTP 400") && cliutil.LooksLikeAuthError(msg):
				return mcplib.NewToolResultError("authentication error: " + cliutil.SanitizeErrorBody(msg) +
					"\nhint: the API rejected the request — this usually means auth is missing or invalid." +
					"\n      Set your API key with: export MCP_CLOUDFLARE_API_KEY=\"your-token-here\"" +
					"\n      Run 'mcp-cloudflare-pp-cli doctor' to check auth status."), nil
			case strings.Contains(msg, "HTTP 401"):
				return mcplib.NewToolResultError("authentication failed: " + cliutil.SanitizeErrorBody(msg) +
					"\nhint: check your API key." +
					"\n      Set your API key with: export MCP_CLOUDFLARE_API_KEY=\"your-token-here\"" +
					"\n      Run 'mcp-cloudflare-pp-cli doctor' to check auth status."), nil
			case strings.Contains(msg, "HTTP 403"):
				return mcplib.NewToolResultError("permission denied: " + cliutil.SanitizeErrorBody(msg) +
					"\nhint: your credentials are valid but lack access to this resource. Check that they have the required permissions and match the API's expected auth scheme." +
					"\n      Set your API key with: export MCP_CLOUDFLARE_API_KEY=\"your-token-here\"" +
					"\n      Run 'mcp-cloudflare-pp-cli doctor' to check auth status."), nil
			case strings.Contains(msg, "HTTP 404"):
				if method == "DELETE" {
					return mcplib.NewToolResultText("already deleted (no-op)"), nil
				}
				return mcplib.NewToolResultError("not found: " + msg), nil
			case strings.Contains(msg, "HTTP 429"):
				return mcplib.NewToolResultError("rate limited: " + msg), nil
			default:
				return mcplib.NewToolResultError(msg), nil
			}
		}

		if binaryResponse {
			encoded := base64.StdEncoding.EncodeToString(data)
			out, err := json.Marshal(map[string]any{
				"content_encoding": "base64",
				"data_base64":      encoded,
				"byte_count":       len(data),
			})
			if err != nil {
				return mcplib.NewToolResultError(fmt.Sprintf("encoding binary result: %v", err)), nil
			}
			if len(out) > bound.MaxBytes {
				return mcplib.NewToolResultError(fmt.Sprintf("binary response is too large for MCP text output: %d response bytes encode to %d base64 bytes and %d MCP result bytes, exceeding the %d byte budget. Use the companion CLI command with --output <file> to save the payload locally.", len(data), len(encoded), len(out), bound.MaxBytes)), nil
			}
			return mcplib.NewToolResultText(string(out)), nil
		}
		if pageConfig.CursorParam != "" {
			return mcpToolPageResultText(method, data, pageConfig, mcpCursor), nil
		}
		return mcpToolResultText(method, data), nil
	}
}

func mcpToolResultText(method string, data json.RawMessage) *mcplib.CallToolResult {
	return mcplib.NewToolResultText(bound.EndpointResponse(method, data))
}

func mcpToolPageResultText(method string, data json.RawMessage, pageConfig mcpPageConfig, cursor string) *mcplib.CallToolResult {
	return mcplib.NewToolResultText(bound.EndpointPageResponse(method, data, bound.PageOptions{
		Cursor:         cursor,
		CursorParam:    pageConfig.CursorParam,
		NextCursorPath: pageConfig.NextCursorPath,
	}))
}

func newMCPClient() (*client.Client, error) {
	cfg, err := newMCPConfig()
	if err != nil {
		return nil, err
	}
	return newMCPClientFromConfig(cfg), nil
}

func newMCPConfig() (*config.Config, error) {
	cfg, err := config.Load("")
	if err != nil {
		return nil, fmt.Errorf("loading config: %w", err)
	}
	return cfg, nil
}

func newMCPClientFromConfig(cfg *config.Config) *client.Client {
	c := client.New(cfg, 60*time.Second, defaultMCPRateLimit)
	// Agents calling through MCP need fresh data every call. The on-disk
	// response cache survives across MCP server invocations, so a
	// DELETE/PATCH followed by a GET would otherwise return the
	// pre-mutation snapshot for up to the cache TTL. The interactive CLI
	// constructs its own client and is unaffected.
	c.NoCache = true
	return c
}

func mcpDBPath() (string, error) {
	dir, err := cliutil.DataDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, "data.db"), nil
}

type mcpStoreStatusKind string

const (
	mcpStoreStatusEmpty mcpStoreStatusKind = "empty"
	mcpStoreStatusReady mcpStoreStatusKind = "ready"
)

func openMCPReadOnlyStore(path string) (*store.Store, *mcplib.CallToolResult) {
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return nil, mcplib.NewToolResultError(mcpMissingStoreMessage(path))
		}
		return nil, mcplib.NewToolResultError(fmt.Sprintf("checking local data store %s: %v", path, err))
	}
	db, err := store.OpenReadOnly(path)
	if err != nil {
		return nil, mcplib.NewToolResultError(fmt.Sprintf("opening local data store %s: %v. Run mcp-cloudflare-pp-cli sync to refresh the store, or use live endpoint MCP tools for unsynced data.", path, err))
	}
	return db, nil
}

func mcpMissingStoreMessage(path string) string {
	return fmt.Sprintf("No local data store found at %s. Run mcp-cloudflare-pp-cli sync before using MCP search/sql, or use live endpoint MCP tools for unsynced data.", path)
}

func mcpStoreStatus(db *store.Store) (mcpStoreStatusKind, error) {
	status, err := db.Status()
	if err != nil {
		return "", err
	}
	if len(status) == 0 {
		return mcpStoreStatusEmpty, nil
	}
	return mcpStoreStatusReady, nil
}

func mcpEmptyStoreNextStep() string {
	return "Run mcp-cloudflare-pp-cli sync to populate the local SQLite store before using MCP search/sql."
}

func handleSearch(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
	args := req.GetArguments()
	query, ok := args["query"].(string)
	if !ok || query == "" {
		return mcplib.NewToolResultError("query is required"), nil
	}

	limit := 25
	if v, ok := args["limit"].(float64); ok && v > 0 {
		limit = int(v)
	}

	path, err := mcpDBPath()
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("resolving database: %v", err)), nil
	}
	db, toolErr := openMCPReadOnlyStore(path)
	if toolErr != nil {
		return toolErr, nil
	}
	defer db.Close()

	results, err := db.Search(query, limit)
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("search failed: %v", err)), nil
	}
	storeStatus, err := mcpStoreStatus(db)
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("reading store status: %v", err)), nil
	}

	return toolResultJSON(mcpSearchEnvelope(results, storeStatus))
}

func mcpSearchEnvelope(results []json.RawMessage, storeStatus mcpStoreStatusKind) map[string]any {
	if results == nil {
		results = []json.RawMessage{}
	}
	out := map[string]any{
		"count":        len(results),
		"results":      results,
		"store_status": storeStatus,
		"resumable":    false,
	}
	if len(results) == 0 {
		if storeStatus == mcpStoreStatusEmpty {
			out["next_step"] = mcpEmptyStoreNextStep()
		} else {
			out["next_step"] = "No local search matches. Try a broader query, a lower-specificity FTS expression, or sync again if data may be stale."
		}
	}
	return out
}

// validateReadOnlyQuery gates the MCP sql tool. The agent contract advertised
// to the host is ReadOnlyHintAnnotation(true); a false annotation on a
// mutating tool lets MCP hosts auto-approve writes and is treated as a real
// bug per the project's agent-native security model.
//
// The gate rejects multi-statement input, then applies an allowlist (SELECT or
// WITH only) AFTER stripping the leading whitespace, line comments, block
// comments, and semicolons that SQLite itself ignores before parsing. A naive
// HasPrefix check on a keyword blocklist is bypassable by prefixing the
// dangerous statement with "/* x */" or "-- x\n"; a naive leading-keyword
// allowlist is bypassable by appending "; ATTACH DATABASE ...". Combined with
// the empirical fact that modernc.org/sqlite's mode=ro does NOT block VACUUM
// INTO (writes a snapshot to a new file) or ATTACH DATABASE (opens a separate
// writable handle), either bypass produces silent exfiltration to an
// attacker-chosen path.
//
// SELECT and WITH are the only allowed leading keywords. WITH supports
// SELECT-form CTEs; CTE-wrapped writes ("WITH x AS (...) INSERT ...") are
// caught by OpenReadOnly's mode=ro one layer down. PRAGMA, ATTACH, VACUUM,
// and every other DDL/DML keyword fail at this gate before reaching SQLite.
func validateReadOnlyQuery(query string) error {
	stripped := stripLeadingSQLNoise(query)
	if hasTrailingSQLStatement(stripped) {
		return fmt.Errorf("only a single SELECT or WITH statement is allowed")
	}
	upper := strings.ToUpper(stripped)
	if !strings.HasPrefix(upper, "SELECT") && !strings.HasPrefix(upper, "WITH") {
		return fmt.Errorf("only SELECT queries are allowed")
	}
	return nil
}

// stripLeadingSQLNoise removes leading whitespace, SQL line comments
// (-- to end of line), block comments (/* ... */), and statement
// separators (;) from query. SQLite skips these before parsing the first
// keyword, so a security gate that does not strip them mismatches what the
// driver actually executes.
func stripLeadingSQLNoise(query string) string {
	for {
		query = strings.TrimLeft(query, " \t\r\n;")
		switch {
		case strings.HasPrefix(query, "--"):
			if idx := strings.IndexByte(query, '\n'); idx >= 0 {
				query = query[idx+1:]
				continue
			}
			return ""
		case strings.HasPrefix(query, "/*"):
			if idx := strings.Index(query[2:], "*/"); idx >= 0 {
				query = query[2+idx+2:]
				continue
			}
			return ""
		default:
			return query
		}
	}
}

// hasTrailingSQLStatement reports whether query contains a statement
// terminator followed by more executable SQL. A trailing semicolon is allowed;
// a second statement is not. Semicolons inside string literals, quoted
// identifiers, bracket identifiers, and comments are ignored to match SQLite's
// parser shape closely enough for this security gate.
func hasTrailingSQLStatement(query string) bool {
	inSingle := false
	inDouble := false
	inBacktick := false
	inBracket := false
	inLineComment := false
	inBlockComment := false

	for i := 0; i < len(query); i++ {
		ch := query[i]
		next := byte(0)
		if i+1 < len(query) {
			next = query[i+1]
		}

		switch {
		case inLineComment:
			if ch == '\n' {
				inLineComment = false
			}
			continue
		case inBlockComment:
			if ch == '*' && next == '/' {
				inBlockComment = false
				i++
			}
			continue
		case inSingle:
			if ch == '\'' {
				if next == '\'' {
					i++
					continue
				}
				inSingle = false
			}
			continue
		case inDouble:
			if ch == '"' {
				if next == '"' {
					i++
					continue
				}
				inDouble = false
			}
			continue
		case inBacktick:
			if ch == '`' {
				if next == '`' {
					i++
					continue
				}
				inBacktick = false
			}
			continue
		case inBracket:
			if ch == ']' {
				inBracket = false
			}
			continue
		}

		switch {
		case ch == '-' && next == '-':
			inLineComment = true
			i++
		case ch == '/' && next == '*':
			inBlockComment = true
			i++
		case ch == '\'':
			inSingle = true
		case ch == '"':
			inDouble = true
		case ch == '`':
			inBacktick = true
		case ch == '[':
			inBracket = true
		case ch == ';':
			if stripLeadingSQLNoise(query[i+1:]) != "" {
				return true
			}
			return false
		}
	}
	return false
}

func handleSQL(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
	args := req.GetArguments()
	query, ok := args["query"].(string)
	if !ok || query == "" {
		return mcplib.NewToolResultError("query is required"), nil
	}

	if err := validateReadOnlyQuery(query); err != nil {
		return mcplib.NewToolResultError(err.Error()), nil
	}

	path, err := mcpDBPath()
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("resolving database: %v", err)), nil
	}
	db, toolErr := openMCPReadOnlyStore(path)
	if toolErr != nil {
		return toolErr, nil
	}
	defer db.Close()

	rows, err := db.DB().QueryContext(ctx, query)
	if err != nil {
		return mcplib.NewToolResultError(mcpSQLQueryError(err)), nil
	}
	defer rows.Close()

	cols, err := rows.Columns()
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("reading columns: %v", err)), nil
	}
	var results []map[string]any
	for rows.Next() {
		values := make([]any, len(cols))
		ptrs := make([]any, len(cols))
		for i := range values {
			ptrs[i] = &values[i]
		}
		if err := rows.Scan(ptrs...); err != nil {
			return mcplib.NewToolResultError(fmt.Sprintf("scanning row: %v", err)), nil
		}
		row := make(map[string]any)
		for i, col := range cols {
			row[col] = values[i]
		}
		results = append(results, row)
	}
	// rows.Next() stops on a mid-iteration error without failing the loop, so
	// skipping rows.Err() would return a truncated result set as success.
	if err := rows.Err(); err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("reading rows: %v", err)), nil
	}
	storeStatus, err := mcpStoreStatus(db)
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("reading store status: %v", err)), nil
	}

	return toolResultJSON(mcpSQLEnvelope(results, cols, storeStatus))
}

func mcpSQLEnvelope(rows []map[string]any, columns []string, storeStatus mcpStoreStatusKind) map[string]any {
	if rows == nil {
		rows = []map[string]any{}
	}
	out := map[string]any{
		"count":        len(rows),
		"columns":      columns,
		"rows":         rows,
		"store_status": storeStatus,
		"resumable":    false,
	}
	if len(rows) == 0 {
		if storeStatus == mcpStoreStatusEmpty {
			out["next_step"] = mcpEmptyStoreNextStep()
		} else {
			out["next_step"] = "The read-only SQL query returned no rows. Check resource_type filters, json_extract paths, or run sync again if data may be stale."
		}
	}
	return out
}

func mcpSQLQueryError(err error) string {
	msg := err.Error()
	if strings.Contains(strings.ToLower(msg), "no such table") {
		return fmt.Sprintf("query failed: %v. Synced records live in resources(resource_type, id, data), not one SQL table per resource. Filter by resource_type, for example resource_type='items', and read JSON fields with json_extract(data,'$.field').", err)
	}
	return fmt.Sprintf("query failed: %v", err)
}

// toolResultJSON renders v as the indented JSON body of an MCP text result,
// surfacing a marshal failure as a tool error instead of empty content.
func toolResultJSON(v any) (*mcplib.CallToolResult, error) {
	text, err := bound.JSON(v)
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("encoding result: %v", err)), nil
	}
	return mcplib.NewToolResultText(text), nil
}

func handleContext(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
	paths := map[string]string{}
	if dir, err := cliutil.ConfigDir(); err == nil {
		paths["config_dir"] = dir
	}
	if dir, err := cliutil.DataDir(); err == nil {
		paths["data_dir"] = dir
	}
	if dir, err := cliutil.StateDir(); err == nil {
		paths["state_dir"] = dir
	}
	if dir, err := cliutil.CacheDir(); err == nil {
		paths["cache_dir"] = dir
	}
	ctx := map[string]any{
		"api":         "mcp-cloudflare",
		"description": "Golden fixture exercising x-mcp on an OpenAPI spec.",
		"archetype":   "generic",
		"tool_count":  1,
		"paths":       paths,
		// tool_surface tells agents which surface a capability lives on.
		"tool_surface": "MCP exposes typed endpoint tools plus a runtime mirror of user-facing CLI commands. Endpoint tools keep typed schemas; command-mirror tools shell out to the companion mcp-cloudflare-pp-cli binary.",
		"auth": map[string]any{
			"type": "api_key",
			"env_vars": []map[string]any{
				{
					"name":        "MCP_CLOUDFLARE_API_KEY",
					"kind":        "per_call",
					"required":    true,
					"sensitive":   true,
					"description": "Set to your API credential.",
				},
			},
		},
		"resources": []map[string]any{
			{
				"name":        "items",
				"description": "Manage items",
				"endpoints":   []string{"list"},
				"syncable":    true,
			},
		},
		"query_tips": []string{
			"Pagination uses cursor-based paging. Pass after parameter for subsequent pages.",
			"Control page size with the limit parameter (default 100).",
			"Use the sql tool for ad-hoc analysis on synced data. Run sync first to populate the local database.",
			"Use the search tool for full-text search across all synced resources. Faster than iterating list endpoints.",
			"Prefer sql/search over repeated API calls when the data is already synced.",
		},
	}
	return toolResultJSON(ctx)
}

// RegisterNovelFeatureTools is kept as a compatibility no-op for older MCP
// mains. New generated mains call RegisterTools only; RegisterTools now
// includes the runtime Cobra-tree mirror.
func RegisterNovelFeatureTools(s *server.MCPServer) {
	_ = s
}
