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.
- {cited_cli-0.5.1 → cited_cli-0.5.2}/PKG-INFO +5 -5
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/pyproject.toml +1 -1
- cited_cli-0.5.2/packages/core/src/cited_core/__init__.py +1 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/api/client.py +10 -2
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/api/endpoints.py +4 -0
- cited_cli-0.5.2/packages/core/src/cited_core/free_audit.py +39 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/README.md +5 -4
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/SKILL.md +4 -3
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/pyproject.toml +3 -3
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/plan_gating.py +1 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tool_changelog.yaml +16 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/audit.py +37 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_plan_gating.py +1 -1
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_surface_invariants.py +1 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/pyproject.toml +4 -4
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/audit.py +93 -1
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_feature_parity.py +1 -0
- cited_cli-0.5.2/tests/test_free_audit.py +173 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_mcp_server.py +1 -0
- cited_cli-0.5.1/packages/core/src/cited_core/__init__.py +0 -1
- {cited_cli-0.5.1 → cited_cli-0.5.2}/.github/workflows/changelog-guard.yml +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/.github/workflows/ci.yml +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/.github/workflows/release.yml +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/.gitignore +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/CLAUDE.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/DEMO.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/FUTURE.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/LICENSE +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/README.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/TEST_RUNBOOK.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/.claude-plugin/marketplace.json +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/.claude-plugin/plugin.json +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/.mcp.json +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/CLAUDE_DESKTOP_SETUP.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/agents/geo-analyst.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/commands/audit.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/commands/recommend.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/commands/status.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/settings.json +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/business-setup/SKILL.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/explore/SKILL.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/geo-audit/SKILL.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/cited-plugins/skills/geo-recommend/SKILL.md +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/api/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/auth/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/auth/oauth_server.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/auth/store.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/config/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/config/constants.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/config/manager.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/core/src/cited_core/errors.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/__main__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/auth_provider.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/context.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/remote.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/server.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tool_manager.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/_helpers.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/action_plan.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/agent.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/analytics.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/auth.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/billing.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/business.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/changelog.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/hq.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/job.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/recommend.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/src/cited_mcp/tools/solution.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/conftest.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_auth_provider.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/packages/mcp/tests/test_tools.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/scripts/dev-install.sh +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/scripts/mcp_test_harness.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/__main__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/api/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/api/client.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/api/endpoints.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/app.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/auth/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/auth/oauth_server.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/auth/store.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/agent.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/analytics.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/auth.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/business.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/config_cmd.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/hq.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/job.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/mcp_cmd.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/named_audit.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/recommend.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/commands/solution.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/config/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/config/constants.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/config/manager.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/context.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/server.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/_helpers.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/audit.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/auth.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/business.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/job.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/recommend.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/mcp/tools/solution.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/formatter.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/progress.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/output/tables.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/utils/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/utils/errors.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/src/cited_cli/utils/interactive.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/__init__.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/conftest.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_api_client.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_auth.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_config.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_live_pipeline.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_mcp_tools.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_output.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_pipeline.py +0 -0
- {cited_cli-0.5.1 → cited_cli-0.5.2}/tests/test_version.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cited-cli
|
|
3
|
-
Version: 0.5.
|
|
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.
|
|
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.
|
|
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.
|
|
20
|
+
Requires-Dist: cited-mcp>=0.5.2; extra == 'mcp'
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
# Cited CLI
|
|
@@ -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(
|
|
76
|
-
|
|
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
|
|
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 — **
|
|
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) — **
|
|
236
|
-
| **Pro** | Everything in Scale + usage stats, HQ dashboard, analytics, agent API — **
|
|
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 (
|
|
54
|
-
- **Scale**: Full write access — create/update/delete, solutions, export, cancel (
|
|
55
|
-
- **Pro**: Everything + HQ dashboard, analytics, agent API (
|
|
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.
|
|
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.
|
|
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
|
]
|
|
@@ -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) ==
|
|
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."""
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cited-cli"
|
|
7
|
-
version = "0.5.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|