// Copyright 2026 printing-press-golden. 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"
	"os"
	"path/filepath"
	"strings"
	"time"

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

// RegisterTools registers all API operations as MCP tools.
func RegisterTools(s *server.MCPServer) {
	s.AddTool(
		mcplib.NewTool("currencies_list",
			mcplib.WithDescription("List supported currencies. Returns array of Currency."),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/currencies", false, nil, []mcpParamBinding{}, []string{}),
	)
	s.AddTool(
		mcplib.NewTool("projects_create",
			mcplib.WithDescription("Create project. Required: name, visibility. Optional: owner_email. Returns the new Project."),
			mcplib.WithString("name", mcplib.Required(), mcplib.Description("Name")),
			mcplib.WithString("owner_email", mcplib.Description("Owner email")),
			mcplib.WithString("visibility", mcplib.Required(), mcplib.Description("Visibility")),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("POST", "/projects", false, nil, []mcpParamBinding{{PublicName: "name", WireName: "name", Location: "body"}, {PublicName: "owner_email", WireName: "owner_email", Location: "body"}, {PublicName: "visibility", WireName: "visibility", Location: "body"}}, []string{}),
	)
	s.AddTool(
		mcplib.NewTool("projects_get",
			mcplib.WithDescription("Get project. Required: projectId."),
			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/projects/{projectId}", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}}, []string{"projectId"}),
	)
	s.AddTool(
		mcplib.NewTool("projects_list",
			mcplib.WithDescription("List projects. Optional: status, limit (default: 25), cursor. Returns array of Project."),
			mcplib.WithString("status", mcplib.Description("Status")),
			mcplib.WithNumber("limit", mcplib.Description("Limit")),
			mcplib.WithString("cursor", mcplib.Description("Cursor")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/projects", false, nil, []mcpParamBinding{{PublicName: "status", WireName: "status", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{}),
	)
	s.AddTool(
		mcplib.NewTool("projects_avatar_upload-project",
			mcplib.WithDescription("Upload project avatar. Required: projectId. Optional: overwrite, caption, file."),
			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
			mcplib.WithBoolean("overwrite", mcplib.Description("Overwrite")),
			mcplib.WithString("caption", mcplib.Description("Caption")),
			mcplib.WithString("file", mcplib.Description("File")),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("PUT", "/projects/{projectId}/avatar", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path", RequestContentType: "multipart/form-data"}, {PublicName: "overwrite", WireName: "overwrite", Location: "query", RequestContentType: "multipart/form-data"}, {PublicName: "caption", WireName: "caption", Location: "body", RequestContentType: "multipart/form-data"}, {PublicName: "file", WireName: "file", Location: "body", Format: "binary", RequestContentType: "multipart/form-data"}}, []string{"projectId"}),
	)
	s.AddTool(
		mcplib.NewTool("projects_tasks_list-project",
			mcplib.WithDescription("List project tasks. Required: projectId. Optional: priority, limit (default: 50), cursor. Returns array of Task."),
			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
			mcplib.WithString("priority", mcplib.Description("Priority")),
			mcplib.WithNumber("limit", mcplib.Description("Limit")),
			mcplib.WithString("cursor", mcplib.Description("Cursor")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/projects/{projectId}/tasks", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}, {PublicName: "priority", WireName: "priority", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{"projectId"}),
	)
	s.AddTool(
		mcplib.NewTool("projects_tasks_update-project",
			mcplib.WithDescription("Update project task. Required: projectId, taskId. Optional: notify, completed, priority (plus 1 more). Partial update."),
			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
			mcplib.WithString("taskId", mcplib.Required(), mcplib.Description("Task id")),
			mcplib.WithBoolean("notify", mcplib.Description("Notify")),
			mcplib.WithBoolean("completed", mcplib.Description("Completed")),
			mcplib.WithString("priority", mcplib.Description("Priority")),
			mcplib.WithString("title", mcplib.Description("Title")),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("PATCH", "/projects/{projectId}/tasks/{taskId}", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}, {PublicName: "taskId", WireName: "taskId", Location: "path"}, {PublicName: "notify", WireName: "notify", Location: "query"}, {PublicName: "completed", WireName: "completed", Location: "body"}, {PublicName: "priority", WireName: "priority", Location: "body"}, {PublicName: "title", WireName: "title", Location: "body"}}, []string{"projectId", "taskId"}),
	)
	s.AddTool(
		mcplib.NewTool("public_get-status",
			mcplib.WithDescription("Get public service status. (public)"),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/public/status", false, nil, []mcpParamBinding{}, []string{}),
	)
	s.AddTool(
		mcplib.NewTool("reports_export_report-year",
			mcplib.WithDescription("Download the annual report as a binary file. Required: year."),
			mcplib.WithNumber("year", mcplib.Required(), mcplib.Description("Year")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/reports/{year}/export", true, map[string]string{"Accept": "application/octet-stream"}, []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
	)
	s.AddTool(
		mcplib.NewTool("reports_summary_get-report-year",
			mcplib.WithDescription("Get a report summary for a year. Required: year."),
			mcplib.WithNumber("year", mcplib.Required(), mcplib.Description("Year")),
			mcplib.WithReadOnlyHintAnnotation(true),
			mcplib.WithDestructiveHintAnnotation(false),
			mcplib.WithOpenWorldHintAnnotation(true),
		),
		makeAPIHandler("GET", "/reports/{year}/summary", false, nil, []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
	)
	// 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). Tables match resource names.")),
			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
	Format             string
	RequestContentType string
}

func mcpMultipartFieldValue(v any) string {
	if s, ok := v.(string); ok {
		return s
	}
	if data, err := json.Marshal(v); err == nil {
		return string(data)
	}
	return fmt.Sprintf("%v", v)
}

// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
func makeAPIHandler(method, pathTemplate string, binaryResponse bool, headerOverrides map[string]string, 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)
		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"
		}
		multipartFields := make(map[string]string)
		multipartFileFields := make(map[string]string)
		multipart := false
		for _, binding := range bindings {
			knownArgs[binding.PublicName] = true
			if strings.EqualFold(binding.RequestContentType, "multipart/form-data") {
				multipart = 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, fmt.Sprintf("%v", v), 1)
			case "body":
				bodyArgs[binding.WireName] = v
				if multipart {
					if strings.EqualFold(binding.Format, "binary") {
						multipartFileFields[binding.WireName] = fmt.Sprintf("%v", v)
					} else {
						multipartFields[binding.WireName] = mcpMultipartFieldValue(v)
					}
				}
			default:
				params[binding.WireName] = fmt.Sprintf("%v", 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, fmt.Sprintf("%v", v), 1)
			}
		}

		for k, v := range args {
			if pathParams[k] || knownArgs[k] {
				continue
			}
			switch method {
			case "POST", "PUT", "PATCH":
				bodyArgs[k] = v
				if multipart {
					multipartFields[k] = mcpMultipartFieldValue(v)
				}
			default:
				params[k] = fmt.Sprintf("%v", v)
			}
		}

		var data json.RawMessage
		switch method {
		case "GET":
			if len(headers) > 0 {
				data, err = c.GetWithHeaders(path, params, headers)
				break
			}
			data, err = c.Get(path, params)
		case "POST":
			if multipart {
				if len(headers) > 0 {
					data, _, err = c.PostMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
					break
				}
				data, _, err = c.PostMultipartWithParams(path, params, multipartFields, multipartFileFields)
				break
			}
			if len(headers) > 0 {
				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
				break
			}
			data, _, err = c.PostWithParams(path, params, bodyArgs)
		case "PUT":
			if multipart {
				if len(headers) > 0 {
					data, _, err = c.PutMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
					break
				}
				data, _, err = c.PutMultipartWithParams(path, params, multipartFields, multipartFileFields)
				break
			}
			if len(headers) > 0 {
				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
				break
			}
			data, _, err = c.PutWithParams(path, params, bodyArgs)
		case "PATCH":
			if multipart {
				if len(headers) > 0 {
					data, _, err = c.PatchMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
					break
				}
				data, _, err = c.PatchMultipartWithParams(path, params, multipartFields, multipartFileFields)
				break
			}
			if len(headers) > 0 {
				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
				break
			}
			data, _, err = c.PatchWithParams(path, params, bodyArgs)
		case "DELETE":
			if len(headers) > 0 {
				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
				break
			}
			data, _, err = c.DeleteWithParams(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: export PRINTING_PRESS_GOLDEN_API_KEY=<your-key>" +
					"\n      Run 'printing-press-golden-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 it with: export PRINTING_PRESS_GOLDEN_API_KEY=<your-key>" +
					"\n      Run 'printing-press-golden-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." +
					"\n      Set it with: export PRINTING_PRESS_GOLDEN_API_KEY=<your-key>" +
					"\n      Run 'printing-press-golden-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
			}
		}

		// For GET responses, wrap bare arrays with count metadata
		if method == "GET" {
			trimmed := strings.TrimSpace(string(data))
			if len(trimmed) > 0 && trimmed[0] == '[' {
				var items []json.RawMessage
				if json.Unmarshal(data, &items) == nil {
					wrapped := map[string]any{
						"count": len(items),
						"items": items,
					}
					out, _ := json.Marshal(wrapped)
					return mcplib.NewToolResultText(string(out)), nil
				}
			}
		}
		if binaryResponse {
			out, _ := json.Marshal(map[string]any{
				"content_encoding": "base64",
				"data_base64":      base64.StdEncoding.EncodeToString(data),
				"byte_count":       len(data),
			})
			return mcplib.NewToolResultText(string(out)), nil
		}
		return mcplib.NewToolResultText(string(data)), nil
	}
}

func newMCPClient() (*client.Client, error) {
	home, _ := os.UserHomeDir()
	cfgPath := filepath.Join(home, ".config", "printing-press-golden-pp-cli", "config.toml")
	cfg, err := config.Load(cfgPath)
	if err != nil {
		return nil, fmt.Errorf("loading config: %w", err)
	}
	c := client.New(cfg, 30*time.Second, 0)
	// 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, nil
}

func dbPath() string {
	home, _ := os.UserHomeDir()
	return filepath.Join(home, ".local", "share", "printing-press-golden-pp-cli", "data.db")
}

// Note: MCP tools use their own dbPath() because they are in a separate package (main, not cli).
// The CLI's defaultDBPath() in the cli package uses the same canonical path.

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)
	}

	db, err := store.OpenReadOnly(dbPath())
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("opening database: %v", err)), nil
	}
	defer db.Close()

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

	data, _ := json.MarshalIndent(results, "", "  ")
	return mcplib.NewToolResultText(string(data)), nil
}

// 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 is an allowlist (SELECT or WITH only) applied 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" — TrimSpace strips outer whitespace but does not
// understand SQL comment syntax. 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),
// such a 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 {
	upper := strings.ToUpper(stripLeadingSQLNoise(query))
	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
		}
	}
}

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
	}

	db, err := store.OpenReadOnly(dbPath())
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("opening database: %v", err)), nil
	}
	defer db.Close()

	rows, err := db.Query(query)
	if err != nil {
		return mcplib.NewToolResultError(fmt.Sprintf("query failed: %v", err)), nil
	}
	defer rows.Close()

	cols, _ := rows.Columns()
	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]
		}
		rows.Scan(ptrs...)
		row := make(map[string]any)
		for i, col := range cols {
			row[col] = values[i]
		}
		results = append(results, row)
	}

	data, _ := json.MarshalIndent(results, "", "  ")
	return mcplib.NewToolResultText(string(data)), nil
}

func handleContext(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
	ctx := map[string]any{
		"api":         "printing-press-golden",
		"description": "Purpose-built fixture for golden generation coverage.",
		"archetype":   "project-management",
		"tool_count":  10,
		// 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 printing-press-golden-pp-cli binary.",
		"auth": map[string]any{
			"type": "api_key",
			"env_vars": []map[string]any{
				{
					"name":        "PRINTING_PRESS_GOLDEN_API_KEY",
					"kind":        "per_call",
					"required":    true,
					"sensitive":   true,
					"description": "Set to your API credential.",
				},
			},
		},
		"resources": []map[string]any{
			{
				"name":        "currencies",
				"description": "Manage currencies",
				"endpoints":   []string{"list"},
				"syncable":    true,
			},
			{
				"name":        "projects",
				"description": "Manage projects",
				"endpoints":   []string{"create", "get", "list"},
				"syncable":    true,
				"searchable":  true,
			},
			{
				"name":        "public",
				"description": "Manage public",
				"endpoints":   []string{"get-status"},
			},
			{
				"name":        "reports",
				"description": "Manage reports",
				"endpoints":   []string{},
			},
		},
		"query_tips": []string{
			"Pagination uses cursor-based paging. Pass cursor 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.",
		},
		"playbook": []map[string]string{
			{"topic": "Finding stale work", "insight": "Use the stale command or sql query to find items not updated recently. More reliable than scanning list results manually."},
			{"topic": "Load analysis", "insight": "When analyzing team workload, filter by assignee and status. Raw counts without status filtering are misleading."},
			{"topic": "Bulk operations", "insight": "For bulk status changes, prefer update endpoints over delete+create. Most PM APIs track history on updates."},
		},
	}
	data, _ := json.MarshalIndent(ctx, "", "  ")
	return mcplib.NewToolResultText(string(data)), nil
}

// 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
}
