cited-cli 0.5.1__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {cited_cli-0.5.1 → cited_cli-0.5.2}/PKG-INFO +5 -5
  2. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/pyproject.toml +1 -1
  3. cited_cli-0.5.2/packages/core/src/cited_core/__init__.py +1 -0
  4. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/api/client.py +10 -2
  5. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/api/endpoints.py +4 -0
  6. cited_cli-0.5.2/packages/core/src/cited_core/free_audit.py +39 -0
  7. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/README.md +5 -4
  8. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/SKILL.md +4 -3
  9. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/pyproject.toml +3 -3
  10. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/plan_gating.py +1 -0
  11. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tool_changelog.yaml +16 -0
  12. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/audit.py +37 -0
  13. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_plan_gating.py +1 -1
  14. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_surface_invariants.py +1 -0
  15. {cited_cli-0.5.1 → cited_cli-0.5.2}/pyproject.toml +4 -4
  16. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/audit.py +93 -1
  17. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_feature_parity.py +1 -0
  18. cited_cli-0.5.2/tests/test_free_audit.py +173 -0
  19. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_mcp_server.py +1 -0
  20. cited_cli-0.5.1/packages/core/src/cited_core/__init__.py +0 -1
  21. {cited_cli-0.5.1 → cited_cli-0.5.2}/.github/workflows/changelog-guard.yml +0 -0
  22. {cited_cli-0.5.1 → cited_cli-0.5.2}/.github/workflows/ci.yml +0 -0
  23. {cited_cli-0.5.1 → cited_cli-0.5.2}/.github/workflows/release.yml +0 -0
  24. {cited_cli-0.5.1 → cited_cli-0.5.2}/.gitignore +0 -0
  25. {cited_cli-0.5.1 → cited_cli-0.5.2}/CLAUDE.md +0 -0
  26. {cited_cli-0.5.1 → cited_cli-0.5.2}/DEMO.md +0 -0
  27. {cited_cli-0.5.1 → cited_cli-0.5.2}/FUTURE.md +0 -0
  28. {cited_cli-0.5.1 → cited_cli-0.5.2}/LICENSE +0 -0
  29. {cited_cli-0.5.1 → cited_cli-0.5.2}/README.md +0 -0
  30. {cited_cli-0.5.1 → cited_cli-0.5.2}/TEST_RUNBOOK.md +0 -0
  31. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/.claude-plugin/marketplace.json +0 -0
  32. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/.claude-plugin/plugin.json +0 -0
  33. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/.mcp.json +0 -0
  34. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/CLAUDE_DESKTOP_SETUP.md +0 -0
  35. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/agents/geo-analyst.md +0 -0
  36. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/commands/audit.md +0 -0
  37. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/commands/recommend.md +0 -0
  38. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/commands/status.md +0 -0
  39. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/settings.json +0 -0
  40. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/business-setup/SKILL.md +0 -0
  41. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/explore/SKILL.md +0 -0
  42. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/geo-audit/SKILL.md +0 -0
  43. {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/geo-recommend/SKILL.md +0 -0
  44. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/api/__init__.py +0 -0
  45. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/auth/__init__.py +0 -0
  46. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/auth/oauth_server.py +0 -0
  47. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/auth/store.py +0 -0
  48. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/config/__init__.py +0 -0
  49. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/config/constants.py +0 -0
  50. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/config/manager.py +0 -0
  51. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/errors.py +0 -0
  52. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/__init__.py +0 -0
  53. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/__main__.py +0 -0
  54. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/auth_provider.py +0 -0
  55. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/context.py +0 -0
  56. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/remote.py +0 -0
  57. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/server.py +0 -0
  58. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tool_manager.py +0 -0
  59. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/__init__.py +0 -0
  60. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/_helpers.py +0 -0
  61. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/action_plan.py +0 -0
  62. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/agent.py +0 -0
  63. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/analytics.py +0 -0
  64. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/auth.py +0 -0
  65. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/billing.py +0 -0
  66. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/business.py +0 -0
  67. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/changelog.py +0 -0
  68. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/hq.py +0 -0
  69. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/job.py +0 -0
  70. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/recommend.py +0 -0
  71. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/solution.py +0 -0
  72. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/conftest.py +0 -0
  73. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_auth_provider.py +0 -0
  74. {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_tools.py +0 -0
  75. {cited_cli-0.5.1 → cited_cli-0.5.2}/scripts/dev-install.sh +0 -0
  76. {cited_cli-0.5.1 → cited_cli-0.5.2}/scripts/mcp_test_harness.py +0 -0
  77. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/__init__.py +0 -0
  78. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/__main__.py +0 -0
  79. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/api/__init__.py +0 -0
  80. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/api/client.py +0 -0
  81. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/api/endpoints.py +0 -0
  82. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/app.py +0 -0
  83. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/auth/__init__.py +0 -0
  84. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/auth/oauth_server.py +0 -0
  85. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/auth/store.py +0 -0
  86. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/__init__.py +0 -0
  87. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/agent.py +0 -0
  88. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/analytics.py +0 -0
  89. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/auth.py +0 -0
  90. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/business.py +0 -0
  91. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/config_cmd.py +0 -0
  92. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/hq.py +0 -0
  93. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/job.py +0 -0
  94. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/mcp_cmd.py +0 -0
  95. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/named_audit.py +0 -0
  96. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/recommend.py +0 -0
  97. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/solution.py +0 -0
  98. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/config/__init__.py +0 -0
  99. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/config/constants.py +0 -0
  100. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/config/manager.py +0 -0
  101. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/__init__.py +0 -0
  102. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/context.py +0 -0
  103. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/server.py +0 -0
  104. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/__init__.py +0 -0
  105. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/_helpers.py +0 -0
  106. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/audit.py +0 -0
  107. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/auth.py +0 -0
  108. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/business.py +0 -0
  109. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/job.py +0 -0
  110. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/recommend.py +0 -0
  111. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/solution.py +0 -0
  112. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/__init__.py +0 -0
  113. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/formatter.py +0 -0
  114. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/progress.py +0 -0
  115. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/tables.py +0 -0
  116. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/utils/__init__.py +0 -0
  117. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/utils/errors.py +0 -0
  118. {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/utils/interactive.py +0 -0
  119. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/__init__.py +0 -0
  120. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/conftest.py +0 -0
  121. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_api_client.py +0 -0
  122. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_auth.py +0 -0
  123. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_config.py +0 -0
  124. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_live_pipeline.py +0 -0
  125. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_mcp_tools.py +0 -0
  126. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_output.py +0 -0
  127. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_pipeline.py +0 -0
  128. {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_version.py +0 -0
@@ -1,23 +1,23 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: cited-cli
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: CLI for the Cited GEO platform
5
5
  Author-email: Cited <support@youcited.com>
6
6
  License-Expression: LicenseRef-Proprietary
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.11
9
- Requires-Dist: cited-core>=0.5.1
9
+ Requires-Dist: cited-core>=0.5.2
10
10
  Requires-Dist: rich>=13.0.0
11
11
  Requires-Dist: typer>=0.15.0
12
12
  Provides-Extra: dev
13
- Requires-Dist: cited-mcp>=0.5.1; extra == 'dev'
13
+ Requires-Dist: cited-mcp>=0.5.2; extra == 'dev'
14
14
  Requires-Dist: mypy>=1.10; extra == 'dev'
15
15
  Requires-Dist: pytest>=8.0; extra == 'dev'
16
16
  Requires-Dist: respx>=0.21; extra == 'dev'
17
17
  Requires-Dist: ruff>=0.4; extra == 'dev'
18
18
  Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
19
19
  Provides-Extra: mcp
20
- Requires-Dist: cited-mcp>=0.5.1; extra == 'mcp'
20
+ Requires-Dist: cited-mcp>=0.5.2; extra == 'mcp'
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  # Cited CLI
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cited-core"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "Core library for the Cited GEO platform"
9
9
  license = "LicenseRef-Proprietary"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1 @@
1
+ __version__ = "0.5.2"
@@ -72,8 +72,16 @@ class CitedClient:
72
72
  message = response.text or message
73
73
  raise CitedAPIError(response.status_code, str(message), error_code)
74
74
 
75
- def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
76
- response = self._client.get(path, params=params)
75
+ def get(
76
+ self,
77
+ path: str,
78
+ params: dict[str, Any] | None = None,
79
+ timeout: float | None = None,
80
+ ) -> Any:
81
+ kwargs: dict[str, Any] = {"params": params}
82
+ if timeout is not None:
83
+ kwargs["timeout"] = timeout
84
+ response = self._client.get(path, **kwargs)
77
85
  return self._handle_response(response)
78
86
 
79
87
  def post(
@@ -43,6 +43,10 @@ AUDIT_EXPORT_PDF = "/audit/{job_id}/export/pdf"
43
43
  AUDIT_EXPORT_URL = "/audit/{job_id}/export/url"
44
44
  AUDIT_HISTORY = "/audit/history"
45
45
  AUDIT_CANCEL = "/audit/{job_id}/cancel"
46
+
47
+ # Free audit (no login; same audit the youcited.com homepage prompt runs)
48
+ FREE_AUDIT = "/free-audit"
49
+ FREE_AUDIT_REPORT = "/free-audit/report/{token}"
46
50
  AUDIT_DELETE = "/audit/{job_id}"
47
51
  AUDIT_PROVIDERS = "/audit/{job_id}/providers"
48
52
  AUDIT_QUESTION_DETAIL = "/audit/{job_id}/question/{question_id}"
@@ -0,0 +1,39 @@
1
+ """Free audit: the no-login GEO audit behind the youcited.com homepage prompt.
2
+
3
+ ``GET /free-audit?url=...`` starts an audit (or returns the one already run for
4
+ that domain in the last 24 hours) and hands back a ``report_url``. Fetching the
5
+ report returns status while the audit runs and the full report once it is done.
6
+ Neither call needs a token, so these work before the user has an account.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from cited_core.api import endpoints
14
+ from cited_core.api.client import LONG_TIMEOUT, CitedClient
15
+
16
+
17
+ def report_token(report_url: str) -> str:
18
+ """Extract the report token from a ``report_url`` returned by the API."""
19
+ return report_url.rstrip("/").rsplit("/", 1)[-1]
20
+
21
+
22
+ def check_free_audit(client: CitedClient, url: str) -> dict[str, Any]:
23
+ """Start (or resume) the free audit for ``url`` and return its current report.
24
+
25
+ Safe to call repeatedly: the API reuses the same audit for a domain, so
26
+ calling again later returns progress or, once complete, the full report.
27
+ """
28
+ # Starting a new audit generates its questions in-request, which can be slow.
29
+ started = client.get(endpoints.FREE_AUDIT, params={"url": url}, timeout=LONG_TIMEOUT)
30
+ token = report_token(started["report_url"])
31
+ report: dict[str, Any] = client.get(endpoints.FREE_AUDIT_REPORT.format(token=token))
32
+ report.setdefault("report_url", started["report_url"])
33
+ report.setdefault("share_url", started.get("share_url"))
34
+ report.setdefault("domain", started.get("domain"))
35
+ return report
36
+
37
+
38
+ def is_finished(report: dict[str, Any]) -> bool:
39
+ return report.get("status") not in ("pending", "processing")
@@ -1,6 +1,6 @@
1
1
  # cited-mcp
2
2
 
3
- MCP server for the [Cited](https://youcited.com) Generative Engine Optimization (GEO) platform. Exposes 73 tools that let AI assistants like Claude manage businesses, run GEO audits, generate recommendations, work a prioritized action plan, verify fixes via the Validation Engine, and create solutions — all through the [Model Context Protocol](https://modelcontextprotocol.io/).
3
+ MCP server for the [Cited](https://youcited.com) Generative Engine Optimization (GEO) platform. Exposes 74 tools that let AI assistants like Claude manage businesses, run GEO audits, generate recommendations, work a prioritized action plan, verify fixes via the Validation Engine, and create solutions — all through the [Model Context Protocol](https://modelcontextprotocol.io/).
4
4
 
5
5
  ## Install
6
6
 
@@ -146,6 +146,7 @@ Cited adds new tools regularly. To access new tools after a release, **disconnec
146
146
  ### Audits
147
147
  | Tool | Description |
148
148
  |------|-------------|
149
+ | `free_audit` | Free GEO audit of any website, no login needed. Call again with the same URL for progress and the full report |
149
150
  | `start_audit` | Run a GEO audit using a template |
150
151
  | `get_audit_status` | Check audit job status |
151
152
  | `get_audit_result` | Get completed audit results |
@@ -231,9 +232,9 @@ Tools are gated by subscription tier. All tiers can read data; write operations
231
232
 
232
233
  | Tier | Tools Available |
233
234
  |------|----------------|
234
- | **Growth** (entry) | Auth, list/get businesses, crawl, health scores, audits (start/status/result/list), all recommendation tools, action-plan reads (get_action_plan, get_quick_wins), Validation Engine reads, job status — **31 tools** |
235
- | **Scale** | Everything in Growth + create/update/delete businesses, create/update/delete audit templates, all solution tools, batch solutions, export audit, cancel job, action-plan writes (mark_action_done, dismiss_action, get_action_progress) — **62 tools** |
236
- | **Pro** | Everything in Scale + usage stats, HQ dashboard, analytics, agent API — **73 tools** |
235
+ | **Growth** (entry) | Auth, list/get businesses, crawl, health scores, audits (start/status/result/list), all recommendation tools, action-plan reads (get_action_plan, get_quick_wins), Validation Engine reads, job status, free audit — **32 tools** |
236
+ | **Scale** | Everything in Growth + create/update/delete businesses, create/update/delete audit templates, all solution tools, batch solutions, export audit, cancel job, action-plan writes (mark_action_done, dismiss_action, get_action_progress) — **63 tools** |
237
+ | **Pro** | Everything in Scale + usage stats, HQ dashboard, analytics, agent API — **74 tools** |
237
238
 
238
239
  When a user calls a tool above their plan, the server returns a structured error with an upgrade link:
239
240
 
@@ -19,6 +19,7 @@ The typical flow is: **Business Setup → Audit → Recommendations → Solution
19
19
 
20
20
  ### 2. Run an Audit
21
21
  - `create_audit_template` — Define what questions to audit (e.g., "What is [business]?", "Best [industry] tools")
22
+ - `free_audit` — Free GEO audit of any website, no login or plan needed. Use it when the user has no Cited account yet. Call again with the same `url` every ~30 seconds until `status` is `completed` (2-4 minutes).
22
23
  - `start_audit` — Launch the audit using a template (2-4 minutes). Poll `get_audit_status` every 30-60 seconds.
23
24
  - `get_audit_result` — Returns a summary by default (aggregate KPIs, competitor leaderboard, question IDs). Use `full=True` for detailed per-question citations — warning: full results can be very large.
24
25
 
@@ -50,9 +51,9 @@ The typical flow is: **Business Setup → Audit → Recommendations → Solution
50
51
 
51
52
  Tools are gated by subscription tier. If a tool is above the user's plan, it returns an upgrade message with a billing link.
52
53
 
53
- - **Growth** (entry tier): Read-only — list/get businesses, run audits, view recommendations (31 tools)
54
- - **Scale**: Full write access — create/update/delete, solutions, export, cancel (62 tools)
55
- - **Pro**: Everything + HQ dashboard, analytics, agent API (73 tools)
54
+ - **Growth** (entry tier): Read-only — list/get businesses, run audits, view recommendations (32 tools)
55
+ - **Scale**: Full write access — create/update/delete, solutions, export, cancel (63 tools)
56
+ - **Pro**: Everything + HQ dashboard, analytics, agent API (74 tools)
56
57
 
57
58
  Always call `check_auth_status` first — it shows the user's plan and remaining limits.
58
59
 
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cited-mcp"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "MCP server for the Cited GEO platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
11
11
  requires-python = ">=3.11"
12
12
  authors = [{ name = "Cited", email = "support@youcited.com" }]
13
13
  dependencies = [
14
- "cited-core>=0.5.1",
15
- "mcp>=1.27.0",
14
+ "cited-core>=0.5.2",
15
+ "mcp>=1.27.0,<2",
16
16
  "PyJWT>=2.8.0",
17
17
  "PyYAML>=6.0",
18
18
  ]
@@ -165,6 +165,7 @@ _BASE_TOOLS: set[str] = {
165
165
  "get_pricing",
166
166
  "upgrade_plan",
167
167
  "whats_new",
168
+ "free_audit",
168
169
  "list_businesses",
169
170
  "get_business",
170
171
  "crawl_business",
@@ -21,6 +21,22 @@
21
21
  # this file.
22
22
 
23
23
  versions:
24
+ - version: "0.5.2"
25
+ released: "2026-10-14"
26
+ # Adds free_audit: the no-login GEO audit behind the youcited.com homepage
27
+ # prompt, so agents can audit a site before the user has an account.
28
+ fingerprint: "8e42f3842abe"
29
+ tools_added:
30
+ - name: free_audit
31
+ description: |
32
+ Free GEO visibility audit of any website, no login or plan needed.
33
+ The first call starts it; call again with the same url for progress
34
+ and the full report (questions, whether the site was cited, which
35
+ domains AI cited instead). Requires the cited backend with
36
+ GET /free-audit (YouCited/cited#132).
37
+ tools_changed: []
38
+ tools_removed: []
39
+
24
40
  - version: "0.5.1"
25
41
  released: "2026-05-18"
26
42
  # Telemetry-only release: tool surface is unchanged (fingerprint matches
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  from typing import Any
4
5
 
5
6
  from mcp.server.fastmcp import Context
6
7
  from mcp.types import ToolAnnotations
7
8
 
8
9
  from cited_core.api import endpoints
10
+ from cited_core.api.client import CitedClient
9
11
  from cited_core.errors import CitedAPIError
12
+ from cited_core.free_audit import check_free_audit, is_finished
10
13
  from cited_mcp.context import CitedContext
11
14
  from cited_mcp.server import mcp
12
15
  from cited_mcp.tools._helpers import (
@@ -366,3 +369,37 @@ async def list_audits(
366
369
  return cited_ctx.client.get(endpoints.AUDIT_HISTORY, params=params)
367
370
  except CitedAPIError as e:
368
371
  return _api_error_response(e)
372
+
373
+
374
+ @mcp.tool(
375
+ title="Free Audit",
376
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=True, openWorldHint=True), # noqa: E501
377
+ )
378
+ @log_tool_call
379
+ async def free_audit(ctx: Context[Any, CitedContext, Any], url: str) -> Any:
380
+ """Run a free GEO visibility audit for any website. No login or plan needed.
381
+
382
+ Checks how often AI assistants cite the site for a handful of buyer
383
+ questions and which sites they cite instead. The first call starts the
384
+ audit (2-4 minutes); call again with the same url to get progress, and the
385
+ full report once status is "completed". Re-running a domain within 24 hours
386
+ returns the same audit.
387
+
388
+ Args:
389
+ ctx: MCP context
390
+ url: Website to audit, e.g. "example.com"
391
+ """
392
+ cited_ctx = _get_ctx(ctx)
393
+ # Anonymous client: no user cookie. The hosted server's agent key (when set)
394
+ # exempts it from the backend's per-IP limit, which all its users share.
395
+ agent_key = os.environ.get("CITED_AGENT_API_KEY") or cited_ctx.client.agent_api_key
396
+ client = CitedClient(base_url=cited_ctx.api_url, agent_api_key=agent_key)
397
+ try:
398
+ report = check_free_audit(client, url)
399
+ except CitedAPIError as e:
400
+ return _api_error_response(e)
401
+ finally:
402
+ client.close()
403
+ if not is_finished(report):
404
+ report["hint"] = "Audit still running. Call free_audit again with the same url in ~30s."
405
+ return _truncate_response(report)
@@ -204,7 +204,7 @@ class TestToolsForTier:
204
204
  # All tools should be available to pro. Bump this count whenever a tool
205
205
  # is added/removed to/from any tier set.
206
206
  tools = tools_for_tier("pro")
207
- assert len(tools) == 73
207
+ assert len(tools) == 74
208
208
 
209
209
  def test_all_registered_tools_are_in_tier_sets(self):
210
210
  """Every registered MCP tool must appear in exactly one tier set."""
@@ -63,6 +63,7 @@ EXPECTED_TOOLS_V0_5_0: set[str] = {
63
63
  "get_pricing",
64
64
  "upgrade_plan",
65
65
  "whats_new",
66
+ "free_audit",
66
67
  "get_usage_stats",
67
68
  # Businesses
68
69
  "list_businesses",
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cited-cli"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "CLI for the Cited GEO platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
11
11
  requires-python = ">=3.11"
12
12
  authors = [{ name = "Cited", email = "support@youcited.com" }]
13
13
  dependencies = [
14
- "cited-core>=0.5.1",
14
+ "cited-core>=0.5.2",
15
15
  "typer>=0.15.0",
16
16
  "rich>=13.0.0",
17
17
  ]
@@ -20,13 +20,13 @@ dependencies = [
20
20
  cited = "cited_cli.app:main"
21
21
 
22
22
  [project.optional-dependencies]
23
- mcp = ["cited-mcp>=0.5.1"]
23
+ mcp = ["cited-mcp>=0.5.2"]
24
24
  dev = [
25
25
  "pytest>=8.0",
26
26
  "respx>=0.21",
27
27
  "ruff>=0.4",
28
28
  "mypy>=1.10",
29
- "cited-mcp>=0.5.1",
29
+ "cited-mcp>=0.5.2",
30
30
  "types-PyYAML>=6.0",
31
31
  ]
32
32
 
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Annotated
3
+ import time
4
+ from contextlib import nullcontext
5
+ from typing import Annotated, Any
4
6
 
5
7
  import typer
6
8
  from rich.console import Console
@@ -13,6 +15,7 @@ from cited_core.api import endpoints
13
15
  from cited_core.api.client import LONG_TIMEOUT, CitedClient
14
16
  from cited_core.auth.store import TokenStore
15
17
  from cited_core.config.manager import ConfigManager
18
+ from cited_core.free_audit import check_free_audit, is_finished
16
19
 
17
20
  audit_app = typer.Typer(name="audit", help="Run and manage audits.")
18
21
 
@@ -34,6 +37,95 @@ def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient, str]:
34
37
  return out, CitedClient(base_url=api_url, token=token), env
35
38
 
36
39
 
40
+ FREE_AUDIT_POLL_SECONDS = 15
41
+ FREE_AUDIT_MAX_WAIT_SECONDS = 15 * 60
42
+
43
+
44
+ def _render_free_audit(data: dict[str, Any], console: Console) -> None:
45
+ if data.get("status") != "completed":
46
+ render_kv(
47
+ "Free Audit",
48
+ {
49
+ "Domain": data.get("domain", ""),
50
+ "Status": data.get("status", ""),
51
+ "Progress": f"{data.get('progress', 0)}%",
52
+ "Report": data.get("report_url", ""),
53
+ },
54
+ console,
55
+ )
56
+ if data.get("error"):
57
+ console.print(f"[red]{data['error']}[/red]")
58
+ return
59
+
60
+ summary = data.get("summary", {})
61
+ render_kv(
62
+ f"Free Audit: {data.get('domain', '')}",
63
+ {
64
+ "Your site cited in": (
65
+ f"{summary.get('questions_where_brand_cited', 0)} of "
66
+ f"{summary.get('questions_checked', 0)} questions"
67
+ ),
68
+ "High-risk questions": summary.get("high_risk_questions", 0),
69
+ "Share of voice": f"{summary.get('share_of_voice') or 0:.0f}%",
70
+ "AI providers": ", ".join(data.get("providers_used") or []),
71
+ "Full report": data.get("share_url", ""),
72
+ },
73
+ console,
74
+ )
75
+ render_table(
76
+ "Questions",
77
+ ["Question", "Risk", "You cited", "AI cited instead"],
78
+ [
79
+ [
80
+ q.get("question", ""),
81
+ q.get("risk_level") or "",
82
+ "yes" if q.get("brand_cited") else "no",
83
+ ", ".join(q.get("cited_domains") or []),
84
+ ]
85
+ for q in data.get("questions", [])
86
+ ],
87
+ console,
88
+ )
89
+ console.print("\nFor more questions and fixes, create an account: [bold]cited register[/bold]")
90
+
91
+
92
+ @audit_app.command("free")
93
+ def audit_free(
94
+ ctx: typer.Context,
95
+ url: Annotated[str | None, typer.Argument(help="Website to audit, e.g. example.com")] = None,
96
+ wait: Annotated[
97
+ bool,
98
+ typer.Option("--wait/--no-wait", help="Wait for the audit to finish (2-4 minutes)"),
99
+ ] = True,
100
+ ) -> None:
101
+ """Run a free GEO visibility audit for any website. No login needed."""
102
+ obj = ctx.obj or {}
103
+ out: OutputContext = obj.get("output", OutputContext())
104
+ cfg: ConfigManager = obj.get("config", ConfigManager())
105
+ api_url = cfg.get_api_url(obj.get("profile", "default"), obj.get("env_override"))
106
+ url = prompt_if_missing(url, "URL", "Website to audit", out)
107
+
108
+ client = CitedClient(base_url=api_url)
109
+ try:
110
+ deadline = time.monotonic() + FREE_AUDIT_MAX_WAIT_SECONDS
111
+ # No spinner in JSON mode: stdout must carry only the JSON document.
112
+ status = nullcontext() if out.json_mode else out.console.status("Running free audit...")
113
+ with status as spinner:
114
+ data = check_free_audit(client, url)
115
+ while wait and not is_finished(data) and time.monotonic() < deadline:
116
+ if spinner is not None:
117
+ spinner.update(f"Running free audit... {data.get('progress', 0)}%")
118
+ time.sleep(FREE_AUDIT_POLL_SECONDS)
119
+ data = check_free_audit(client, url)
120
+ print_result(data, out, human_formatter=_render_free_audit)
121
+ if not out.json_mode and not is_finished(data):
122
+ out.console.print(f"\nCheck again later: [bold]cited audit free {url}[/bold]")
123
+ except CitedAPIError as e:
124
+ handle_api_error(e, out.json_mode)
125
+ finally:
126
+ client.close()
127
+
128
+
37
129
  @audit_app.command("start")
38
130
  def audit_start(
39
131
  ctx: typer.Context,
@@ -108,6 +108,7 @@ _CLI_TO_MCP: dict[str, str | list[str]] = {
108
108
  "audit result": "get_audit_result",
109
109
  "audit list": "list_audits",
110
110
  "audit export": "export_audit",
111
+ "audit free": "free_audit",
111
112
  # Recommendations
112
113
  "recommend start": "start_recommendation",
113
114
  "recommend status": "get_recommendation_status",
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import httpx
6
+ import pytest
7
+ import respx
8
+
9
+ from tests.test_mcp_tools import _make_cited_ctx, _make_mcp_ctx, _mcp_available, _run # noqa: F401
10
+
11
+ DEV_API = "https://dev.youcited.com"
12
+ TOKEN = "instant_a1b2c3d4_0123456789abcdef"
13
+
14
+ STARTED = {
15
+ "domain": "acme-plumbing.com",
16
+ "job_id": "a1b2c3d4e5",
17
+ "status": "pending",
18
+ "report_url": f"{DEV_API}/free-audit/report/{TOKEN}",
19
+ "share_url": f"https://youcited.com/share/{TOKEN}",
20
+ "poll_after_seconds": 30,
21
+ }
22
+ PROCESSING = {"status": "processing", "progress": 40, "poll_after_seconds": 30}
23
+ COMPLETED = {
24
+ "status": "completed",
25
+ "domain": "acme-plumbing.com",
26
+ "providers_used": ["perplexity"],
27
+ "summary": {
28
+ "questions_checked": 2,
29
+ "questions_where_brand_cited": 1,
30
+ "high_risk_questions": 1,
31
+ "share_of_voice": 35.0,
32
+ },
33
+ "questions": [
34
+ {
35
+ "question": "Best plumber in Austin?",
36
+ "risk_level": "high",
37
+ "brand_cited": False,
38
+ "cited_domains": ["yelp.com", "angi.com"],
39
+ }
40
+ ],
41
+ "share_url": f"https://youcited.com/share/{TOKEN}",
42
+ }
43
+
44
+
45
+ def _isolate_config(tmp_path, monkeypatch) -> None:
46
+ """Point config and credentials at an empty temp dir: no login exists."""
47
+ config_dir = tmp_path / ".cited"
48
+ monkeypatch.setattr("cited_core.config.constants.CONFIG_DIR", config_dir)
49
+ monkeypatch.setattr("cited_core.config.constants.CONFIG_FILE", config_dir / "config.toml")
50
+ monkeypatch.setattr(
51
+ "cited_core.config.constants.CREDENTIALS_FILE", config_dir / "credentials.json"
52
+ )
53
+ monkeypatch.setattr("cited_core.auth.store.CONFIG_DIR", config_dir)
54
+ monkeypatch.setattr(
55
+ "cited_core.auth.store.CREDENTIALS_FILE", config_dir / "credentials.json"
56
+ )
57
+ monkeypatch.setattr("cited_core.auth.store.TokenStore._has_keyring", lambda self: False)
58
+ monkeypatch.setattr("cited_cli.commands.audit.time.sleep", lambda s: None)
59
+
60
+
61
+ @respx.mock
62
+ def test_cli_audit_free_waits_for_report_without_login(runner, cli_app, tmp_path, monkeypatch):
63
+ _isolate_config(tmp_path, monkeypatch)
64
+ start = respx.get(f"{DEV_API}/free-audit").mock(
65
+ return_value=httpx.Response(202, json=STARTED)
66
+ )
67
+ respx.get(f"{DEV_API}/free-audit/report/{TOKEN}").mock(
68
+ side_effect=[
69
+ httpx.Response(200, json=PROCESSING),
70
+ httpx.Response(200, json=COMPLETED),
71
+ ]
72
+ )
73
+
74
+ result = runner.invoke(
75
+ cli_app, ["--env", "dev", "--json", "audit", "free", "acme-plumbing.com"]
76
+ )
77
+
78
+ assert result.exit_code == 0, result.output
79
+ data = json.loads(result.output)
80
+ assert data["status"] == "completed"
81
+ assert data["summary"]["questions_where_brand_cited"] == 1
82
+ assert start.calls.last.request.url.params["url"] == "acme-plumbing.com"
83
+ assert "cookie" not in start.calls.last.request.headers
84
+
85
+
86
+ @respx.mock
87
+ def test_cli_audit_free_no_wait_returns_progress(runner, cli_app, tmp_path, monkeypatch):
88
+ _isolate_config(tmp_path, monkeypatch)
89
+ respx.get(f"{DEV_API}/free-audit").mock(return_value=httpx.Response(202, json=STARTED))
90
+ report = respx.get(f"{DEV_API}/free-audit/report/{TOKEN}").mock(
91
+ return_value=httpx.Response(200, json=PROCESSING)
92
+ )
93
+
94
+ result = runner.invoke(
95
+ cli_app,
96
+ ["--env", "dev", "--json", "audit", "free", "acme-plumbing.com", "--no-wait"],
97
+ )
98
+
99
+ assert result.exit_code == 0, result.output
100
+ data = json.loads(result.output)
101
+ assert data["status"] == "processing"
102
+ assert data["report_url"] == STARTED["report_url"]
103
+ assert report.call_count == 1
104
+
105
+
106
+ @respx.mock
107
+ def test_cli_audit_free_rate_limited(runner, cli_app, tmp_path, monkeypatch):
108
+ _isolate_config(tmp_path, monkeypatch)
109
+ respx.get(f"{DEV_API}/free-audit").mock(
110
+ return_value=httpx.Response(429, json={"detail": "at today's limit"})
111
+ )
112
+
113
+ result = runner.invoke(cli_app, ["--env", "dev", "audit", "free", "acme-plumbing.com"])
114
+
115
+ assert result.exit_code == 5
116
+
117
+
118
+ @pytest.mark.usefixtures("_mcp_available")
119
+ @respx.mock
120
+ def test_mcp_free_audit_works_without_token():
121
+ from cited_mcp.tools.audit import free_audit
122
+
123
+ respx.get(f"{DEV_API}/free-audit").mock(return_value=httpx.Response(202, json=STARTED))
124
+ respx.get(f"{DEV_API}/free-audit/report/{TOKEN}").mock(
125
+ return_value=httpx.Response(200, json=PROCESSING)
126
+ )
127
+
128
+ ctx = _make_mcp_ctx(_make_cited_ctx(token=None))
129
+ result = _run(free_audit(ctx, url="acme-plumbing.com"))
130
+
131
+ assert result["status"] == "processing"
132
+ assert "call free_audit again" in result["hint"].lower()
133
+
134
+
135
+ @pytest.mark.usefixtures("_mcp_available")
136
+ @respx.mock
137
+ def test_mcp_free_audit_returns_completed_report():
138
+ from cited_mcp.tools.audit import free_audit
139
+
140
+ respx.get(f"{DEV_API}/free-audit").mock(
141
+ return_value=httpx.Response(202, json={**STARTED, "status": "completed"})
142
+ )
143
+ respx.get(f"{DEV_API}/free-audit/report/{TOKEN}").mock(
144
+ return_value=httpx.Response(200, json=COMPLETED)
145
+ )
146
+
147
+ ctx = _make_mcp_ctx(_make_cited_ctx(token=None))
148
+ result = _run(free_audit(ctx, url="acme-plumbing.com"))
149
+
150
+ assert result["status"] == "completed"
151
+ assert result["questions"][0]["cited_domains"] == ["yelp.com", "angi.com"]
152
+ assert "hint" not in result
153
+
154
+
155
+ @pytest.mark.usefixtures("_mcp_available")
156
+ @respx.mock
157
+ def test_mcp_free_audit_sends_server_agent_key_not_user_cookie(monkeypatch):
158
+ from cited_mcp.tools.audit import free_audit
159
+
160
+ monkeypatch.setenv("CITED_AGENT_API_KEY", "server-key")
161
+ start = respx.get(f"{DEV_API}/free-audit").mock(
162
+ return_value=httpx.Response(202, json=STARTED)
163
+ )
164
+ respx.get(f"{DEV_API}/free-audit/report/{TOKEN}").mock(
165
+ return_value=httpx.Response(200, json=PROCESSING)
166
+ )
167
+
168
+ ctx = _make_mcp_ctx(_make_cited_ctx(token="user-jwt"))
169
+ _run(free_audit(ctx, url="acme-plumbing.com"))
170
+
171
+ sent = start.calls.last.request.headers
172
+ assert sent["x-agent-api-key"] == "server-key"
173
+ assert "cookie" not in sent
@@ -87,6 +87,7 @@ EXPECTED_TOOLS = [
87
87
  "upgrade_plan",
88
88
  # Changelog
89
89
  "whats_new",
90
+ "free_audit",
90
91
  # Agent API
91
92
  "get_business_facts",
92
93
  "get_business_claims",
@@ -1 +0,0 @@
1
- __version__ = "0.5.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes