From 6e02186a2b7f3ad2ccd394b2a0e45c8114519434 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Sat, 22 Aug 2026 12:25:03 -0400 Subject: [PATCH] Add ChatGPT plugin submission manifest (#519) --- chatgpt-app-submission.json | 632 ++++++++++++++++++ src/server/mcp/context.ts | 2 - src/server/mcp/formatters.test.ts | 2 - src/server/mcp/formatters.ts | 1 - src/server/mcp/output-schemas.ts | 1 - src/server/mcp/tools/create-project.ts | 1 - .../mcp/tools/dataforseo-research-tools.ts | 12 +- .../mcp/tools/google-analytics-tools.test.ts | 2 +- .../mcp/tools/google-analytics-tools.ts | 20 +- src/server/mcp/tools/list-projects.ts | 1 - src/server/mcp/tools/local-seo-tools.ts | 8 +- src/server/mcp/tools/run-rank-tracker.ts | 2 +- src/server/mcp/tools/save-keywords.ts | 2 +- src/server/mcp/tools/search-console-tools.ts | 4 +- src/server/mcp/tools/site-audit-tools.ts | 6 +- src/server/mcp/tools/whoami.ts | 10 +- 16 files changed, 662 insertions(+), 44 deletions(-) create mode 100644 chatgpt-app-submission.json diff --git a/chatgpt-app-submission.json b/chatgpt-app-submission.json new file mode 100644 index 0000000..b18f10e --- /dev/null +++ b/chatgpt-app-submission.json @@ -0,0 +1,632 @@ +{ + "$schema": "https://developers.openai.com/plugins/schemas/chatgpt-app-submission.v1.json", + "schema_version": 1, + "app_info": { + "display_name": "OpenSEO", + "subtitle": "SEO made simple.", + "description": "Without quality data, AI gives generic advice.\n\nOpenSEO starts at $10/month instead of a $100+ / month like other legacy tools.\n\nIt's approachable for entrepreneurs doing SEO for the first time, but powerful to grow with you as you learn.\n\nYou can do keyword research, competitor research, backlinks, site audits and much more.\n\nConnect with popular integrations like Google Search Console & Analytics.", + "category": "BUSINESS" + }, + "tools": { + "whoami": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves the connected account email, scopes, server mode, and credit balance when the user asks to confirm their connection.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "list_projects": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves projects available to the authenticated OpenSEO user.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "create_project": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Creates a new project record in the user's private OpenSEO workspace.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Adds a project without deleting or overwriting existing data." + } + }, + "get_project_context": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves the selected project's stored context, competitors, key pages, and research log.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "update_project_context": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": true + }, + "justifications": { + "read_only_justification": "Updates stored project context and can add, replace, or remove selected project records.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Can clear sections or remove saved competitors, key pages, custom sections, and research-log entries when explicitly requested." + } + }, + "list_saved_keywords": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves keywords already saved in the selected OpenSEO project.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "research_keywords": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches keyword research and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "save_keywords": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": true + }, + "justifications": { + "read_only_justification": "Saves keywords and can create or change their tag associations in the selected project.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Replacement mode removes existing tag associations before applying the user-approved replacement tags." + } + }, + "get_domain_overview": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches domain metrics and can record metered research usage when a cached result is unavailable.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_domain_keyword_suggestions": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches keyword suggestions and can record metered research usage when a cached result is unavailable.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_backlinks_overview": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches backlink summary data and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_backlinks_profile": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches a bounded page of backlink rows and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_serp_results": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches live search results and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "create_rank_tracker": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Creates a private rank-tracker configuration without starting a check or spending credits.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Adds a tracker without deleting or overwriting existing data." + } + }, + "get_rank_tracker": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves a rank tracker's configuration, keywords, runs, and stored results.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "add_rank_tracking_keywords": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Adds keywords to a private tracker and can enable future scheduled checks after explicit estimate approval.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "The configuration change is reversible by removing the keywords and this call starts no check or charge." + } + }, + "remove_rank_tracking_keywords": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": true + }, + "justifications": { + "read_only_justification": "Removes selected keywords from the tracker's active configuration.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Deletes the active tracking relationships for the selected keyword IDs, although historical snapshots are preserved." + } + }, + "estimate_rank_tracker_cost": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only calculates a cost estimate from the tracker configuration and proposed keyword count.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "run_rank_tracker": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Starts a private rank-check job after validating the user's approved credit ceiling.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Uses an approved metered credit amount but does not delete, overwrite, revoke, send, or publish anything." + } + }, + "get_ranked_keywords": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches ranked-keyword research and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "find_serp_competitors": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches search-competitor research and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "search_local_businesses": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches local-business research and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_local_serp_results": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches a local search-results page and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_google_business_questions": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches public business questions and answers and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_business_profile": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches a public business profile and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_business_reviews": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Starts or resumes private collection of public business reviews and can record metered usage for a new task.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_business_updates": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Starts or resumes private collection of public business posts and can record metered usage for a new task.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "list_business_categories": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves the provider's business-category reference list.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_local_rank_grid": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Runs private local search research across a bounded grid and records usage for each grid point.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_keyword_metrics": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Fetches metrics for known keywords and records resulting usage against the user's OpenSEO credit balance.", + "open_world_justification": "Uses a private SEO research provider and cannot publish or modify publicly visible internet state.", + "destructive_justification": "May use metered research credits but does not delete, overwrite, revoke access, send, or publish anything." + } + }, + "get_search_console_performance": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves performance rows from a user-authorized Search Console property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "inspect_urls": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves URL inspection information from a user-authorized Search Console property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_organic_landing_pages": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves organic landing-page performance from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_page_performance": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves page performance from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_key_events": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves key-event performance from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_search_opportunities": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only combines read-only Search Console and Analytics reports to identify opportunities.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_organic_overview": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves an organic-search overview from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_traffic_acquisition": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves traffic-acquisition data from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_measurement_health": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves measurement and configuration signals from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_ecommerce_performance": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves ecommerce performance from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_site_search": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves site-search behavior from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_google_analytics_audience_breakdown": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves audience dimensions from a user-authorized Analytics property.", + "open_world_justification": "Reads user-authorized Google data and cannot change Google or publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "run_site_audit": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Creates a private audit record and starts a background crawl of the user-selected public site.", + "open_world_justification": "Crawls public pages but cannot alter the site or any other publicly visible internet state.", + "destructive_justification": "Creates audit results without deleting or overwriting existing user data." + } + }, + "get_audit_status": { + "annotations": { + "readOnlyHint": false, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Retrieves audit progress and may reconcile a dead workflow by persisting its audit as failed.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Status reconciliation preserves collected results and does not delete or overwrite user-authored data." + } + }, + "get_audit_issues": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves stored issues from a previously started site audit.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + }, + "get_audit_pages": { + "annotations": { + "readOnlyHint": true, + "openWorldHint": false, + "destructiveHint": false + }, + "justifications": { + "read_only_justification": "Only retrieves stored page results from a previously started site audit.", + "open_world_justification": "Operates only on private OpenSEO state or private provider data and cannot change publicly visible internet state.", + "destructive_justification": "Does not delete, overwrite, revoke access, send messages, or perform irreversible actions." + } + } + }, + "test_cases": [ + { + "description": "List the reviewer's OpenSEO projects and identify the site associated with each one.", + "user_prompt": "Show me my OpenSEO projects and briefly explain which website each project is for.", + "file_attachment_urls": null, + "tools_triggered": "list_projects", + "expected_output": "Returns the available projects with their names, domains, markets, and direct OpenSEO links.", + "expected_output_url": null + }, + { + "description": "Guide an SEO beginner through a focused keyword-research workflow.", + "user_prompt": "I'm new to SEO. Use my OpenSEO project context to research five promising keywords for my business and explain why each one matters.", + "file_attachment_urls": null, + "tools_triggered": "list_projects, get_project_context, research_keywords", + "expected_output": "Returns five focused keyword opportunities with useful metrics and short beginner-friendly reasons.", + "expected_output_url": null + }, + { + "description": "Compare the review project with its saved primary competitor.", + "user_prompt": "Compare my website with the main competitor saved in my OpenSEO project and show me one realistic SEO opportunity.", + "file_attachment_urls": null, + "tools_triggered": "list_projects, get_project_context, get_domain_overview, get_ranked_keywords, get_backlinks_overview", + "expected_output": "Returns a concise comparison grounded in keyword, domain, and backlink evidence with one practical opportunity.", + "expected_output_url": null + }, + { + "description": "Find one improvement opportunity using the connected Search Console property.", + "user_prompt": "Use my connected Search Console data to find striking-distance searches and tell me the first page I should improve.", + "file_attachment_urls": null, + "tools_triggered": "list_projects, get_search_console_performance", + "expected_output": "Returns one recommended page, the supporting query metrics, and a specific improvement to try.", + "expected_output_url": null + }, + { + "description": "Run and complete the site-audit lifecycle for the review project.", + "user_prompt": "Run a technical audit of my OpenSEO project's website, wait for it to finish, and give me the three issues I should fix first.", + "file_attachment_urls": null, + "tools_triggered": "list_projects, run_site_audit, get_audit_status, get_audit_issues", + "expected_output": "Returns three prioritized technical issues with affected pages and clear remediation steps.", + "expected_output_url": null + } + ], + "negative_test_cases": [ + { + "description": "Do not invoke OpenSEO to publish or edit website content.", + "user_prompt": "Publish the recommended article and edit my homepage now.", + "file_attachment_urls": null, + "tools_triggered": null, + "expected_output": "Explain that OpenSEO cannot publish content or edit a website, and offer a draft or implementation checklist instead.", + "expected_output_url": null + }, + { + "description": "Do not invoke OpenSEO to perform a financial transaction.", + "user_prompt": "Buy another $100 of credits and charge my card.", + "file_attachment_urls": null, + "tools_triggered": null, + "expected_output": "Explain that purchasing credits is unavailable through the plugin and direct the user to OpenSEO billing.", + "expected_output_url": null + }, + { + "description": "Do not begin unbounded paid research or save results without confirmation.", + "user_prompt": "Find every keyword my site could rank for in the US and save them all without checking with me.", + "file_attachment_urls": null, + "tools_triggered": null, + "expected_output": "Ask the user to narrow the topic and limit, explain that research uses credits, and do not save anything without confirmation.", + "expected_output_url": null + } + ] +} diff --git a/src/server/mcp/context.ts b/src/server/mcp/context.ts index debc192..ef57c04 100644 --- a/src/server/mcp/context.ts +++ b/src/server/mcp/context.ts @@ -99,7 +99,6 @@ export function buildBillingCustomer( export function buildProjectMeta( context: { - auth: Pick; baseUrl: string; }, projectId: string, @@ -107,7 +106,6 @@ export function buildProjectMeta( params?: Record, ) { return { - organizationId: context.auth.organizationId, projectId, url: path ? buildDashboardUrl(context.baseUrl, path, params) : undefined, }; diff --git a/src/server/mcp/formatters.test.ts b/src/server/mcp/formatters.test.ts index 9aae6b0..dc41d70 100644 --- a/src/server/mcp/formatters.test.ts +++ b/src/server/mcp/formatters.test.ts @@ -26,7 +26,6 @@ describe("mcpResponse", () => { text: "hi", meta: { url: "https://app.openseo.so", - organizationId: undefined, creditsCharged: 0, }, }); @@ -34,7 +33,6 @@ describe("mcpResponse", () => { url: "https://app.openseo.so", creditsCharged: 0, }); - expect(result._meta).not.toHaveProperty("organizationId"); }); it("attaches structuredContent when provided", () => { diff --git a/src/server/mcp/formatters.ts b/src/server/mcp/formatters.ts index 50c7e2a..b3bd204 100644 --- a/src/server/mcp/formatters.ts +++ b/src/server/mcp/formatters.ts @@ -2,7 +2,6 @@ import type { CallToolResult } from "@modelcontextprotocol/server"; type McpResponseMeta = { url?: string; - organizationId?: string; projectId?: string; runId?: string; creditsCharged?: number; diff --git a/src/server/mcp/output-schemas.ts b/src/server/mcp/output-schemas.ts index 5f34690..120522f 100644 --- a/src/server/mcp/output-schemas.ts +++ b/src/server/mcp/output-schemas.ts @@ -16,7 +16,6 @@ export function objectSchema(schema: z.ZodType | z.ZodRawShape | undefined) { const mcpMetaOutputSchema = z .object({ url: z.string().optional(), - organizationId: z.string().optional(), projectId: z.string().optional(), runId: z.string().optional(), creditsCharged: z.number().optional(), diff --git a/src/server/mcp/tools/create-project.ts b/src/server/mcp/tools/create-project.ts index 74f0ed8..f3e7f2e 100644 --- a/src/server/mcp/tools/create-project.ts +++ b/src/server/mcp/tools/create-project.ts @@ -77,7 +77,6 @@ export const createProjectTool = { project.domain ? ` (${project.domain})` : "" } market:${project.locationCode}/${project.languageCode}`, meta: { - organizationId: auth.organizationId, url: buildDashboardUrl(baseUrl, `/p/${project.id}`), }, structuredContent: { diff --git a/src/server/mcp/tools/dataforseo-research-tools.ts b/src/server/mcp/tools/dataforseo-research-tools.ts index 62e5424..5afc071 100644 --- a/src/server/mcp/tools/dataforseo-research-tools.ts +++ b/src/server/mcp/tools/dataforseo-research-tools.ts @@ -792,7 +792,7 @@ export const getRankedKeywordsTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -871,7 +871,7 @@ export const searchLocalBusinessesTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -918,7 +918,7 @@ export const getLocalSerpResultsTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -964,7 +964,7 @@ export const getGoogleBusinessQuestionsTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -1007,7 +1007,7 @@ export const findSerpCompetitorsTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -1066,7 +1066,7 @@ export const getKeywordMetricsTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, diff --git a/src/server/mcp/tools/google-analytics-tools.test.ts b/src/server/mcp/tools/google-analytics-tools.test.ts index 79c8a47..912e5f4 100644 --- a/src/server/mcp/tools/google-analytics-tools.test.ts +++ b/src/server/mcp/tools/google-analytics-tools.test.ts @@ -82,7 +82,7 @@ describe("Google Analytics MCP tools", () => { expect(result.structuredContent).toMatchObject({ status: "ok", rowCount: 1, - meta: { projectId: "project_1", organizationId: "org_123" }, + meta: { projectId: "project_1" }, }); }); diff --git a/src/server/mcp/tools/google-analytics-tools.ts b/src/server/mcp/tools/google-analytics-tools.ts index ed8b7d0..3951ac7 100644 --- a/src/server/mcp/tools/google-analytics-tools.ts +++ b/src/server/mcp/tools/google-analytics-tools.ts @@ -261,7 +261,7 @@ export const getGoogleAnalyticsOrganicLandingPagesTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -295,7 +295,7 @@ export const getGoogleAnalyticsPagePerformanceTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -332,7 +332,7 @@ export const getGoogleAnalyticsKeyEventsTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -363,7 +363,7 @@ export const getSearchOpportunitiesTool = { outputSchema: opportunityOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -399,7 +399,7 @@ export const getGoogleAnalyticsOrganicOverviewTool = { outputSchema: overviewOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -437,7 +437,7 @@ export const getGoogleAnalyticsTrafficAcquisitionTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -474,7 +474,7 @@ export const getGoogleAnalyticsEcommercePerformanceTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -503,7 +503,7 @@ export const getGoogleAnalyticsSiteSearchTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -541,7 +541,7 @@ export const getGoogleAnalyticsAudienceBreakdownTool = { outputSchema: reportOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -571,7 +571,7 @@ export const getGoogleAnalyticsMeasurementHealthTool = { outputSchema: measurementHealthOutputSchema, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, diff --git a/src/server/mcp/tools/list-projects.ts b/src/server/mcp/tools/list-projects.ts index 56a6d29..6ab5064 100644 --- a/src/server/mcp/tools/list-projects.ts +++ b/src/server/mcp/tools/list-projects.ts @@ -46,7 +46,6 @@ export const listProjectsTool = { return mcpResponse({ text: `Projects (${projects.length}):\n${lines.join("\n")}`, meta: { - organizationId: auth.organizationId, url: buildDashboardUrl(baseUrl, "/"), }, structuredContent: { diff --git a/src/server/mcp/tools/local-seo-tools.ts b/src/server/mcp/tools/local-seo-tools.ts index 4efd1f0..9d1bc42 100644 --- a/src/server/mcp/tools/local-seo-tools.ts +++ b/src/server/mcp/tools/local-seo-tools.ts @@ -225,7 +225,7 @@ export const getBusinessProfileTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -374,7 +374,7 @@ export const getBusinessReviewsTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -514,7 +514,7 @@ export const getBusinessUpdatesTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, @@ -908,7 +908,7 @@ export const getLocalRankGridTool = { ...optionalMetaOutputSchema, }, annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, diff --git a/src/server/mcp/tools/run-rank-tracker.ts b/src/server/mcp/tools/run-rank-tracker.ts index 2c9c799..d34d66d 100644 --- a/src/server/mcp/tools/run-rank-tracker.ts +++ b/src/server/mcp/tools/run-rank-tracker.ts @@ -43,7 +43,7 @@ export const runRankTrackerTool = { .passthrough(), annotations: { readOnlyHint: false, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, diff --git a/src/server/mcp/tools/save-keywords.ts b/src/server/mcp/tools/save-keywords.ts index 744059e..c901732 100644 --- a/src/server/mcp/tools/save-keywords.ts +++ b/src/server/mcp/tools/save-keywords.ts @@ -65,7 +65,7 @@ export const saveKeywordsTool = { annotations: { readOnlyHint: false, openWorldHint: false, - destructiveHint: false, + destructiveHint: true, }, }, handler: withMcpProjectAuth(async (args: Args, context) => { diff --git a/src/server/mcp/tools/search-console-tools.ts b/src/server/mcp/tools/search-console-tools.ts index 70541c0..8dbfbdf 100644 --- a/src/server/mcp/tools/search-console-tools.ts +++ b/src/server/mcp/tools/search-console-tools.ts @@ -214,7 +214,7 @@ export const getSearchConsolePerformanceTool = { }, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -350,7 +350,7 @@ export const inspectUrlsTool = { }, annotations: { readOnlyHint: true, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, diff --git a/src/server/mcp/tools/site-audit-tools.ts b/src/server/mcp/tools/site-audit-tools.ts index 75446f8..4862dc4 100644 --- a/src/server/mcp/tools/site-audit-tools.ts +++ b/src/server/mcp/tools/site-audit-tools.ts @@ -78,7 +78,7 @@ export const runSiteAuditTool = { .passthrough(), annotations: { readOnlyHint: false, - openWorldHint: true, + openWorldHint: false, destructiveHint: false, }, }, @@ -156,7 +156,7 @@ export const getAuditStatusTool = { config: { title: "Get site audit status", description: - "Check the progress of a site audit (phase, pages crawled, Lighthouse progress). Free — reads OpenSEO state. Omit auditId for the most recent audit.", + "Check the progress of a site audit (phase, pages crawled, Lighthouse progress). Free — reads OpenSEO state and may reconcile a dead workflow by marking its audit failed. Omit auditId for the most recent audit.", inputSchema: statusInputSchema, outputSchema: z .object({ @@ -165,7 +165,7 @@ export const getAuditStatusTool = { }) .passthrough(), annotations: { - readOnlyHint: true, + readOnlyHint: false, openWorldHint: false, destructiveHint: false, }, diff --git a/src/server/mcp/tools/whoami.ts b/src/server/mcp/tools/whoami.ts index ff410a9..472c9f0 100644 --- a/src/server/mcp/tools/whoami.ts +++ b/src/server/mcp/tools/whoami.ts @@ -23,12 +23,10 @@ export const whoamiTool = { config: { title: "Who am I", description: - "Returns the authenticated user, organization, server mode, token scopes, and current credit balance. Uses no credits — does not call DataForSEO. Use this first to confirm connection context before choosing a project or running paid tools.", + "Confirms the connected OpenSEO account, server mode, token scopes, and current credit balance when the user asks to check their account or connection. Uses no credits — does not call DataForSEO.", inputSchema: {} as Record, outputSchema: { - userId: z.string(), userEmail: z.string(), - organizationId: z.string(), scopes: z.array(z.string()), mode: z.enum(["hosted", "self-hosted"]), creditsRemaining: z.number().nullable(), @@ -55,8 +53,7 @@ export const whoamiTool = { creditsRemaining = (base ?? 0) + (topup ?? 0); } const lines = [ - `User: ${auth.userId} (${auth.userEmail})`, - `Organization: ${auth.organizationId}`, + `Account: ${auth.userEmail}`, `Mode: ${isHosted ? "hosted" : "self-hosted"}`, `Scopes: ${auth.scopes.length > 0 ? auth.scopes.join(", ") : "none"}`, ]; @@ -68,13 +65,10 @@ export const whoamiTool = { return mcpResponse({ text: lines.join("\n"), meta: { - organizationId: auth.organizationId, creditsRemaining: creditsRemaining ?? undefined, }, structuredContent: { - userId: auth.userId, userEmail: auth.userEmail, - organizationId: auth.organizationId, scopes: auth.scopes, mode: isHosted ? "hosted" : "self-hosted", creditsRemaining,