#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { homedir } from "os";
import { join, dirname } from "path";
const API_BASE_URL = "https://api.agentweb.live";
const SIGNUP_URL = "https://agentweb.live/#signup";
const KEY_FILE = join(homedir(), ".agentweb", "key");
/** Load API key from env var, CLI arg, or ~/.agentweb/key (in that order). */
function loadApiKey() {
    const envKey = (process.env.AGENTWEB_API_KEY || "").trim();
    if (envKey)
        return envKey;
    const argvKey = (process.argv[2] || "").trim();
    // Skip flag-style args
    if (argvKey && !argvKey.startsWith("-"))
        return argvKey;
    try {
        if (existsSync(KEY_FILE)) {
            const fromFile = readFileSync(KEY_FILE, "utf8").trim();
            if (fromFile)
                return fromFile;
        }
    }
    catch {
        /* ignore */
    }
    return null;
}
/** Save an API key to ~/.agentweb/key for persistent use. */
function saveApiKey(key) {
    const dir = dirname(KEY_FILE);
    try {
        mkdirSync(dir, { recursive: true, mode: 0o700 });
    }
    catch {
        /* may already exist */
    }
    writeFileSync(KEY_FILE, key.trim() + "\n", { mode: 0o600 });
}
class AgentWebServer {
    server;
    config;
    constructor(config) {
        this.config = config;
        this.server = new Server({
            name: "agentweb-mcp",
            version: "1.5.0",
        }, {
            capabilities: {
                tools: {},
            },
        });
        this.setupHandlers();
        this.setupErrorHandling();
    }
    setupErrorHandling() {
        this.server.onerror = (error) => {
            console.error("[MCP Error]", error);
        };
        process.on("SIGINT", async () => {
            await this.server.close();
            process.exit(0);
        });
    }
    setupHandlers() {
        this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
            tools: this.getTools(),
        }));
        this.server.setRequestHandler(CallToolRequestSchema, async (request) => this.handleToolCall(request));
    }
    getTools() {
        return [
            {
                name: "search_businesses",
                description: "Search the AgentWeb global business directory (11M+ businesses, 233 countries). Returns a markdown prose document by default — ready to hand to an LLM, ~60% fewer tokens than JSON, and better parsing accuracy. Supports text search (q), category filter, city/country filter, and geographic search (lat+lng+radius_km). ONE CALL RETURNS EVERYTHING — name, phone, email, hours, website, address, geo, trust score — no second lookup needed. Use format='json' if you specifically need structured JSON.",
                inputSchema: {
                    type: "object",
                    properties: {
                        q: {
                            type: "string",
                            description: "Text search query, e.g. 'noma', 'thai restaurant', 'dentist near me'",
                        },
                        category: {
                            type: "string",
                            description: "Business category filter (e.g., 'restaurant', 'hotel', 'pharmacy')",
                        },
                        city: {
                            type: "string",
                            description: "City name for location filter",
                        },
                        country: {
                            type: "string",
                            description: "Country code (ISO 3166-1 alpha-2, e.g., 'US', 'GB', 'DK')",
                        },
                        lat: {
                            type: "number",
                            description: "Latitude for geographic search (requires lng and radius_km)",
                        },
                        lng: {
                            type: "number",
                            description: "Longitude for geographic search (requires lat and radius_km)",
                        },
                        radius_km: {
                            type: "number",
                            description: "Search radius in kilometers (used with lat/lng)",
                        },
                        limit: {
                            type: "number",
                            description: "Maximum number of results to return (default: 10, max: 100)",
                            default: 10,
                        },
                        offset: {
                            type: "number",
                            description: "Number of results to skip for pagination",
                            default: 0,
                        },
                        format: {
                            type: "string",
                            enum: ["text", "json"],
                            description: "Response format. 'text' (default) returns markdown prose — recommended for LLM consumption. 'json' returns structured JSON.",
                            default: "text",
                        },
                    },
                },
            },
            {
                name: "get_business",
                description: "Fetch a single business by slug or UUID. Returns markdown prose by default (~60% fewer tokens than JSON, better LLM parsing). Slugs look like 'noma-copenhagen-abc12345' — human-readable, shareable URLs. Use this when you already have an identifier from a previous search. Set format='json' if you need structured data.",
                inputSchema: {
                    type: "object",
                    properties: {
                        id: {
                            type: "string",
                            description: "The business slug (e.g. 'noma-copenhagen-abc12345') or UUID from AgentWeb. Slugs are preferred — they appear in search results and are shareable.",
                        },
                        format: {
                            type: "string",
                            enum: ["text", "json"],
                            description: "Response format. 'text' (default) returns markdown prose. 'json' returns structured JSON.",
                            default: "text",
                        },
                    },
                    required: ["id"],
                },
            },
            {
                name: "agentweb_get_short",
                description: "Fetch a business in the AgentWeb compact shorthand format. Returns the same business as get_business but in ~320 bytes instead of 3-5 KB. Single-letter keys (n=name, p=phone, e=email, w=website, h=hours, g=[lat,lng], s=social, etc.). ~80% fewer tokens for the LLM. Use this whenever you just need contact info — saves significant tokens vs. get_business. Schema: https://api.agentweb.live/v1/schema/short",
                inputSchema: {
                    type: "object",
                    properties: {
                        id: {
                            type: "string",
                            description: "The unique business ID from AgentWeb",
                        },
                    },
                    required: ["id"],
                },
            },
            {
                name: "agentweb_health",
                description: "Check the health status of the AgentWeb API, including total number of businesses and countries available.",
                inputSchema: {
                    type: "object",
                    properties: {},
                },
            },
            {
                name: "agentweb_capabilities",
                description: "Get the machine-readable description of the entire AgentWeb API: every endpoint, every tool, every data source, current live counts. Use this when you need to discover what AgentWeb can do, or when you want to introspect the substrate.",
                inputSchema: {
                    type: "object",
                    properties: {},
                },
            },
            {
                name: "contribute_business",
                description: "Contribute a new business to AgentWeb, OR enrich an existing business with missing data " +
                    "(phone, email, website, hours, address, etc). The directory deduplicates automatically: if a " +
                    "business with the same name already exists at the given coordinates (within 100m), or if a " +
                    "matching phone number is found, your data will be merged into the existing record. Otherwise " +
                    "a new business is created. Use this whenever you find structured business data that AgentWeb " +
                    "doesn't have yet — every contribution improves the network for all agents.",
                inputSchema: {
                    type: "object",
                    properties: {
                        name: { type: "string", description: "Business name (required, min 2 chars)" },
                        phone: { type: "string", description: "Phone number (any format; will be cleaned)" },
                        website: { type: "string", description: "Business website URL" },
                        email: { type: "string", description: "Business email address" },
                        category: { type: "string", description: "Business category, e.g. 'restaurant', 'dentist', 'hotel'" },
                        country_code: { type: "string", description: "ISO 3166-1 alpha-2 country code, e.g. 'US', 'DE'" },
                        lat: { type: "number", description: "Latitude (recommended for accurate dedup)" },
                        lng: { type: "number", description: "Longitude (recommended for accurate dedup)" },
                        address: {
                            type: "object",
                            description: "Address fields: { street, city, postcode, country, full }",
                            properties: {
                                street: { type: "string" },
                                city: { type: "string" },
                                postcode: { type: "string" },
                                country: { type: "string" },
                                full: { type: "string" },
                            },
                        },
                        hours: {
                            type: "object",
                            description: "Opening hours in flexible format (e.g. { monday: '09:00-17:00', ... })",
                        },
                    },
                    required: ["name"],
                },
            },
            {
                name: "report_business",
                description: "Report a business in AgentWeb as closed, having wrong info, or being spam. Used to flag " +
                    "data-quality issues so AgentWeb can correct them. Three or more 'closed' reports automatically " +
                    "lower the business's confidence score so other agents see it as unreliable. Use this whenever " +
                    "you discover that a business is permanently closed, has the wrong phone, wrong address, " +
                    "wrong hours, is a spam listing, or is a duplicate of another business.",
                inputSchema: {
                    type: "object",
                    properties: {
                        business_id: { type: "string", description: "The unique business ID from AgentWeb (required)" },
                        report_type: {
                            type: "string",
                            description: "Type of issue",
                            enum: ["closed", "wrong_phone", "wrong_address", "wrong_hours", "spam", "duplicate", "other"],
                        },
                        details: {
                            type: "string",
                            description: "Optional free-text explanation of what's wrong, max 500 chars",
                        },
                    },
                    required: ["business_id", "report_type"],
                },
            },
            {
                name: "agentweb_leaderboard",
                description: "Get the public leaderboard of top contributing AI agents to AgentWeb (most creates, " +
                    "enrichments, and reports). Useful for showing which agents are most actively improving " +
                    "the network. Returns top contributors plus aggregate counts.",
                inputSchema: {
                    type: "object",
                    properties: {
                        limit: {
                            type: "number",
                            description: "Number of top contributors to return (default: 10, max: 50)",
                            default: 10,
                        },
                    },
                },
            },
        ];
    }
    async makeApiRequest(endpoint, params = {}) {
        const url = new URL(`${API_BASE_URL}${endpoint}`);
        // Add non-null params to query string
        Object.entries(params).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
                url.searchParams.append(key, String(value));
            }
        });
        // API key is REQUIRED on every call (as of backend v2026-04).
        // Sent as header so it doesn't appear in CDN cache keys.
        const response = await fetch(url.toString(), {
            headers: {
                "X-API-Key": this.config.apiKey,
                "User-Agent": "agentweb-mcp/1.5.0",
            },
        });
        if (!response.ok) {
            await this.handleApiError(response);
        }
        return await response.json();
    }
    /** Fetch an endpoint and return the raw response body as a text/markdown string.
     *
     * Used for prose-format responses (the agent-native default). LLMs parse
     * markdown more accurately than JSON and consume ~60% fewer tokens for the
     * same data, so every tool that returns to the LLM should prefer this.
     */
    async makeApiRequestText(endpoint, params = {}) {
        const url = new URL(`${API_BASE_URL}${endpoint}`);
        Object.entries(params).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
                url.searchParams.append(key, String(value));
            }
        });
        const response = await fetch(url.toString(), {
            headers: {
                "X-API-Key": this.config.apiKey,
                "Accept": "text/markdown",
                "User-Agent": "agentweb-mcp/1.5.0",
            },
        });
        if (!response.ok) {
            await this.handleApiError(response);
        }
        return await response.text();
    }
    async makeApiPost(endpoint, body) {
        const url = new URL(`${API_BASE_URL}${endpoint}`);
        const response = await fetch(url.toString(), {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-API-Key": this.config.apiKey,
                "User-Agent": "agentweb-mcp/1.5.0",
            },
            body: JSON.stringify(body),
        });
        if (!response.ok) {
            await this.handleApiError(response);
        }
        return await response.json();
    }
    /** Turn API errors into actionable messages for the agent to surface to the user. */
    async handleApiError(response) {
        const status = response.status;
        let bodyText = "";
        try {
            bodyText = await response.text();
        }
        catch {
            /* ignore */
        }
        if (status === 401) {
            throw new Error(`AgentWeb: API key is invalid or missing. Get a free key at ${SIGNUP_URL} and set AGENTWEB_API_KEY in your MCP client config, then restart.`);
        }
        if (status === 403) {
            throw new Error(`AgentWeb: This API key has been disabled. Contact support or get a new free key at ${SIGNUP_URL}.`);
        }
        if (status === 429) {
            throw new Error(`AgentWeb: Rate limit reached. Free tier = 1,000 reads/day + 120 req/min burst. Upgrade at ${SIGNUP_URL}. Detail: ${bodyText.slice(0, 200)}`);
        }
        if (status === 404) {
            throw new Error(`AgentWeb: Record not found (404). ${bodyText.slice(0, 200)}`);
        }
        throw new Error(`AgentWeb API error (${status}): ${bodyText.slice(0, 300)}`);
    }
    async handleToolCall(request) {
        const { name, arguments: args } = request.params;
        try {
            switch (name) {
                case "search_businesses": {
                    // Default to prose format (markdown) — LLMs parse it ~60% more
                    // token-efficiently than JSON and more accurately. Power users can
                    // opt into JSON via the explicit `format` arg.
                    const wantsJson = args.format === "json";
                    if (wantsJson) {
                        const data = await this.makeApiRequest("/v1/search", args);
                        return {
                            content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
                        };
                    }
                    const params = { ...args, format: "text" };
                    const markdown = await this.makeApiRequestText("/v1/search", params);
                    return {
                        content: [{ type: "text", text: markdown }],
                    };
                }
                case "get_business": {
                    // Accept either a slug (e.g. "noma-copenhagen-abc12345") or a UUID.
                    // The new /v1/b/{id} endpoint transparently resolves both.
                    const identifier = args.id || args.slug;
                    if (!identifier) {
                        throw new Error("Business identifier is required (pass `id` as a slug or UUID)");
                    }
                    const wantsJson = args.format === "json";
                    if (wantsJson) {
                        let data;
                        try {
                            data = await this.makeApiRequest(`/v1/b/${identifier}`);
                        }
                        catch (e) {
                            // Fallback to legacy UUID endpoint
                            data = await this.makeApiRequest(`/v1/r/${identifier}`);
                        }
                        return {
                            content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
                        };
                    }
                    // Prose format — ~60% fewer tokens, better LLM parsing
                    let markdown;
                    try {
                        markdown = await this.makeApiRequestText(`/v1/b/${identifier}/agent.md`);
                    }
                    catch (e) {
                        markdown = await this.makeApiRequestText(`/v1/r/${identifier}/agent.md`);
                    }
                    return {
                        content: [{ type: "text", text: markdown }],
                    };
                }
                case "agentweb_get_short": {
                    if (!args.id) {
                        throw new Error("Business ID is required");
                    }
                    // Compact shorthand JSON — ~320 bytes, single-letter keys.
                    // Only use when you specifically need structured data in minimum
                    // bytes; otherwise prefer get_business (prose, better LLM parsing).
                    const data = await this.makeApiRequest(`/v1/r/${args.id}/agent.json`);
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(data, null, 2),
                            },
                        ],
                    };
                }
                case "agentweb_capabilities": {
                    // Self-describes the entire API surface for agent introspection
                    const data = await this.makeApiRequest("/v1/capabilities");
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(data, null, 2),
                            },
                        ],
                    };
                }
                case "agentweb_health": {
                    const data = await this.makeApiRequest("/v1/health");
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(data, null, 2),
                            },
                        ],
                    };
                }
                case "contribute_business": {
                    if (!args.name) {
                        throw new Error("Business name is required");
                    }
                    const data = await this.makeApiPost("/v1/contribute", args);
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(data, null, 2),
                            },
                        ],
                    };
                }
                case "report_business": {
                    if (!args.business_id || !args.report_type) {
                        throw new Error("business_id and report_type are required");
                    }
                    const data = await this.makeApiPost("/v1/report", args);
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(data, null, 2),
                            },
                        ],
                    };
                }
                case "agentweb_leaderboard": {
                    const data = await this.makeApiRequest("/v1/leaderboard/contributors", { limit: args.limit ?? 10 });
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(data, null, 2),
                            },
                        ],
                    };
                }
                default:
                    throw new Error(`Unknown tool: ${name}`);
            }
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return {
                content: [
                    {
                        type: "text",
                        text: `Error: ${errorMessage}`,
                    },
                ],
                isError: true,
            };
        }
    }
    async run() {
        const transport = new StdioServerTransport();
        await this.server.connect(transport);
        console.error("AgentWeb MCP Server running on stdio");
    }
}
// Main entry point
async function main() {
    // CLI: agentweb-mcp --save-key <key>  →  writes key to ~/.agentweb/key and exits
    const argv = process.argv.slice(2);
    if (argv[0] === "--save-key" && argv[1]) {
        try {
            saveApiKey(argv[1]);
            console.error(`✓ API key saved to ${KEY_FILE}`);
            console.error(`  You can now run 'npx agentweb-mcp' without AGENTWEB_API_KEY.`);
            process.exit(0);
        }
        catch (e) {
            console.error(`Failed to save key: ${e.message}`);
            process.exit(1);
        }
    }
    if (argv[0] === "--help" || argv[0] === "-h") {
        console.error("agentweb-mcp — MCP server for AgentWeb.live");
        console.error("");
        console.error("Usage:");
        console.error("  AGENTWEB_API_KEY=<key> npx agentweb-mcp          Run with key from env");
        console.error("  npx agentweb-mcp <key>                           Run with key as arg");
        console.error("  npx agentweb-mcp --save-key <key>                Save key to ~/.agentweb/key (one-time)");
        console.error("");
        console.error(`Get a free API key at ${SIGNUP_URL}`);
        console.error("Free tier: 1,000 reads/day, unlimited writes. No credit card.");
        process.exit(0);
    }
    const apiKey = loadApiKey();
    if (!apiKey) {
        console.error("┌──────────────────────────────────────────────────────────────┐");
        console.error("│  AgentWeb MCP: API key required                              │");
        console.error("├──────────────────────────────────────────────────────────────┤");
        console.error("│                                                              │");
        console.error(`│  1. Get a free key at ${SIGNUP_URL}               │`);
        console.error("│     (instant, no credit card, no verification)               │");
        console.error("│                                                              │");
        console.error("│  2. Add it to your MCP client config (env var):              │");
        console.error("│       \"env\": { \"AGENTWEB_API_KEY\": \"your_key\" }               │");
        console.error("│                                                              │");
        console.error("│     OR save it once to ~/.agentweb/key:                      │");
        console.error("│       npx agentweb-mcp --save-key your_key                   │");
        console.error("│                                                              │");
        console.error("│  3. Restart your MCP client.                                 │");
        console.error("│                                                              │");
        console.error("└──────────────────────────────────────────────────────────────┘");
        process.exit(1);
    }
    const server = new AgentWebServer({ apiKey });
    await server.run();
}
main().catch((error) => {
    console.error("Fatal error:", error);
    process.exit(1);
});
//# sourceMappingURL=index.js.map