cited-cli 0.3.15__tar.gz → 0.5.1__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 (129) hide show
  1. {cited_cli-0.3.15 → cited_cli-0.5.1}/PKG-INFO +4 -4
  2. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/pyproject.toml +1 -1
  3. cited_cli-0.5.1/packages/core/src/cited_core/__init__.py +1 -0
  4. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/api/endpoints.py +8 -0
  5. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/README.md +4 -4
  6. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/SKILL.md +3 -3
  7. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/pyproject.toml +2 -2
  8. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/plan_gating.py +17 -0
  9. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/remote.py +56 -2
  10. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tool_changelog.yaml +170 -0
  11. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/_helpers.py +103 -10
  12. cited_cli-0.5.1/packages/mcp/src/cited_mcp/tools/business.py +477 -0
  13. cited_cli-0.5.1/packages/mcp/src/cited_mcp/tools/hq.py +799 -0
  14. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/recommend.py +20 -1
  15. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/tests/test_auth_provider.py +41 -32
  16. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/tests/test_plan_gating.py +74 -1
  17. cited_cli-0.5.1/packages/mcp/tests/test_surface_invariants.py +332 -0
  18. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/tests/test_tools.py +684 -3
  19. {cited_cli-0.3.15 → cited_cli-0.5.1}/pyproject.toml +4 -4
  20. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/business.py +104 -0
  21. cited_cli-0.5.1/src/cited_cli/commands/hq.py +578 -0
  22. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/conftest.py +3 -1
  23. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_api_client.py +0 -2
  24. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_auth.py +31 -18
  25. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_config.py +0 -2
  26. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_feature_parity.py +45 -26
  27. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_live_pipeline.py +7 -9
  28. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_mcp_server.py +18 -0
  29. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_mcp_tools.py +39 -20
  30. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_output.py +1 -3
  31. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_pipeline.py +1138 -248
  32. cited_cli-0.3.15/packages/core/src/cited_core/__init__.py +0 -1
  33. cited_cli-0.3.15/packages/mcp/src/cited_mcp/tools/business.py +0 -273
  34. cited_cli-0.3.15/packages/mcp/src/cited_mcp/tools/hq.py +0 -93
  35. cited_cli-0.3.15/src/cited_cli/commands/hq.py +0 -125
  36. {cited_cli-0.3.15 → cited_cli-0.5.1}/.github/workflows/changelog-guard.yml +0 -0
  37. {cited_cli-0.3.15 → cited_cli-0.5.1}/.github/workflows/ci.yml +0 -0
  38. {cited_cli-0.3.15 → cited_cli-0.5.1}/.github/workflows/release.yml +0 -0
  39. {cited_cli-0.3.15 → cited_cli-0.5.1}/.gitignore +0 -0
  40. {cited_cli-0.3.15 → cited_cli-0.5.1}/CLAUDE.md +0 -0
  41. {cited_cli-0.3.15 → cited_cli-0.5.1}/DEMO.md +0 -0
  42. {cited_cli-0.3.15 → cited_cli-0.5.1}/FUTURE.md +0 -0
  43. {cited_cli-0.3.15 → cited_cli-0.5.1}/LICENSE +0 -0
  44. {cited_cli-0.3.15 → cited_cli-0.5.1}/README.md +0 -0
  45. {cited_cli-0.3.15 → cited_cli-0.5.1}/TEST_RUNBOOK.md +0 -0
  46. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/.claude-plugin/marketplace.json +0 -0
  47. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/.claude-plugin/plugin.json +0 -0
  48. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/.mcp.json +0 -0
  49. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/CLAUDE_DESKTOP_SETUP.md +0 -0
  50. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/agents/geo-analyst.md +0 -0
  51. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/commands/audit.md +0 -0
  52. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/commands/recommend.md +0 -0
  53. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/commands/status.md +0 -0
  54. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/settings.json +0 -0
  55. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/skills/business-setup/SKILL.md +0 -0
  56. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/skills/explore/SKILL.md +0 -0
  57. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/skills/geo-audit/SKILL.md +0 -0
  58. {cited_cli-0.3.15 → cited_cli-0.5.1}/cited-plugins/skills/geo-recommend/SKILL.md +0 -0
  59. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/api/__init__.py +0 -0
  60. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/api/client.py +0 -0
  61. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/auth/__init__.py +0 -0
  62. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/auth/oauth_server.py +0 -0
  63. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/auth/store.py +0 -0
  64. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/config/__init__.py +0 -0
  65. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/config/constants.py +0 -0
  66. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/config/manager.py +0 -0
  67. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/core/src/cited_core/errors.py +0 -0
  68. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/__init__.py +0 -0
  69. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/__main__.py +0 -0
  70. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/auth_provider.py +0 -0
  71. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/context.py +0 -0
  72. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/server.py +0 -0
  73. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tool_manager.py +0 -0
  74. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/__init__.py +0 -0
  75. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/action_plan.py +0 -0
  76. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/agent.py +0 -0
  77. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/analytics.py +0 -0
  78. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/audit.py +0 -0
  79. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/auth.py +0 -0
  80. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/billing.py +0 -0
  81. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/changelog.py +0 -0
  82. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/job.py +0 -0
  83. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/src/cited_mcp/tools/solution.py +0 -0
  84. {cited_cli-0.3.15 → cited_cli-0.5.1}/packages/mcp/tests/conftest.py +0 -0
  85. {cited_cli-0.3.15 → cited_cli-0.5.1}/scripts/dev-install.sh +0 -0
  86. {cited_cli-0.3.15 → cited_cli-0.5.1}/scripts/mcp_test_harness.py +0 -0
  87. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/__init__.py +0 -0
  88. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/__main__.py +0 -0
  89. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/api/__init__.py +0 -0
  90. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/api/client.py +0 -0
  91. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/api/endpoints.py +0 -0
  92. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/app.py +0 -0
  93. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/auth/__init__.py +0 -0
  94. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/auth/oauth_server.py +0 -0
  95. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/auth/store.py +0 -0
  96. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/__init__.py +0 -0
  97. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/agent.py +0 -0
  98. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/analytics.py +0 -0
  99. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/audit.py +0 -0
  100. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/auth.py +0 -0
  101. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/config_cmd.py +0 -0
  102. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/job.py +0 -0
  103. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/mcp_cmd.py +0 -0
  104. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/named_audit.py +0 -0
  105. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/recommend.py +0 -0
  106. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/commands/solution.py +0 -0
  107. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/config/__init__.py +0 -0
  108. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/config/constants.py +0 -0
  109. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/config/manager.py +0 -0
  110. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/__init__.py +0 -0
  111. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/context.py +0 -0
  112. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/server.py +0 -0
  113. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/__init__.py +0 -0
  114. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/_helpers.py +0 -0
  115. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/audit.py +0 -0
  116. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/auth.py +0 -0
  117. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/business.py +0 -0
  118. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/job.py +0 -0
  119. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/recommend.py +0 -0
  120. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/mcp/tools/solution.py +0 -0
  121. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/output/__init__.py +0 -0
  122. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/output/formatter.py +0 -0
  123. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/output/progress.py +0 -0
  124. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/output/tables.py +0 -0
  125. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/utils/__init__.py +0 -0
  126. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/utils/errors.py +0 -0
  127. {cited_cli-0.3.15 → cited_cli-0.5.1}/src/cited_cli/utils/interactive.py +0 -0
  128. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/__init__.py +0 -0
  129. {cited_cli-0.3.15 → cited_cli-0.5.1}/tests/test_version.py +0 -0
@@ -1,23 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cited-cli
3
- Version: 0.3.15
3
+ Version: 0.5.1
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.3.15
9
+ Requires-Dist: cited-core>=0.5.1
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.3.15; extra == 'dev'
13
+ Requires-Dist: cited-mcp>=0.5.1; 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.3.15; extra == 'mcp'
20
+ Requires-Dist: cited-mcp>=0.5.1; 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.3.15"
7
+ version = "0.5.1"
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.1"
@@ -23,6 +23,7 @@ BUSINESS = "/businesses/{business_id}"
23
23
  BUSINESS_LOGO = "/businesses/{business_id}/logo"
24
24
  HEALTH_SCORES = "/businesses/{business_id}/health-scores"
25
25
  HEALTH_SCORES_BREAKDOWN = "/businesses/{business_id}/health-scores/breakdown"
26
+ PROFILE_COMPETITORS = "/businesses/{business_id}/profile-competitors"
26
27
 
27
28
  # Business Crawl
28
29
  CRAWL_DATA = "/businesses/{business_id}/crawl-data"
@@ -77,9 +78,16 @@ HQ_PRIORITY_STATUS = "/businesses/{business_id}/hq/priority/{action_id}/status"
77
78
  HQ_PRIORITY_SUMMARY = "/businesses/{business_id}/hq/priority/summary"
78
79
  HQ_EXPORT_PDF = "/businesses/{business_id}/hq/export/pdf"
79
80
  HQ_RECOMPUTE = "/businesses/{business_id}/hq/recompute"
81
+ HQ_OVERVIEW_REFRESH_AUDIT = "/businesses/{business_id}/overview/refresh-audit"
82
+ HQ_OVERVIEW_REFRESH_RECS = "/businesses/{business_id}/overview/refresh-recommendations"
83
+ HQ_OVERVIEW_REFRESH_ALL = "/businesses/{business_id}/overview/refresh-all"
84
+ HQ_AGENT_BRIEF = "/businesses/{business_id}/agent-brief"
80
85
  PERSONAS = "/businesses/{business_id}/personas"
86
+ PERSONA = "/businesses/{business_id}/personas/{persona_id}"
81
87
  PRODUCTS = "/businesses/{business_id}/products"
88
+ PRODUCT = "/businesses/{business_id}/products/{product_id}"
82
89
  BUYER_INTENTS = "/businesses/{business_id}/buyer-intents"
90
+ BUYER_INTENT = "/businesses/{business_id}/buyer-intents/{intent_id}"
83
91
  AGENTIC_READINESS = "/businesses/{business_id}/agentic-readiness"
84
92
  TRUST_SIGNALS = "/businesses/{business_id}/trust-signals"
85
93
 
@@ -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 56 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 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/).
4
4
 
5
5
  ## Install
6
6
 
@@ -231,9 +231,9 @@ Tools are gated by subscription tier. All tiers can read data; write operations
231
231
 
232
232
  | Tier | Tools Available |
233
233
  |------|----------------|
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 — **22 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) — **38 tools** |
236
- | **Pro** | Everything in Scale + usage stats, HQ dashboard, analytics, agent API — **56 tools** |
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** |
237
237
 
238
238
  When a user calls a tool above their plan, the server returns a structured error with an upgrade link:
239
239
 
@@ -50,9 +50,9 @@ The typical flow is: **Business Setup → Audit → Recommendations → Solution
50
50
 
51
51
  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
52
 
53
- - **Growth** (entry tier): Read-only — list/get businesses, run audits, view recommendations (19 tools)
54
- - **Scale**: Full write access — create/update/delete, solutions, export, cancel (32 tools)
55
- - **Pro**: Everything + HQ dashboard, analytics, agent API (42 tools)
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)
56
56
 
57
57
  Always call `check_auth_status` first — it shows the user's plan and remaining limits.
58
58
 
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cited-mcp"
7
- version = "0.3.15"
7
+ version = "0.5.1"
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.3.15",
14
+ "cited-core>=0.5.1",
15
15
  "mcp>=1.27.0",
16
16
  "PyJWT>=2.8.0",
17
17
  "PyYAML>=6.0",
@@ -43,6 +43,18 @@ _SCALE_TOOLS: set[str] = {
43
43
  "mark_action_done",
44
44
  "dismiss_action",
45
45
  "get_action_progress",
46
+ "set_profile_competitors",
47
+ "create_persona",
48
+ "update_persona",
49
+ "delete_persona",
50
+ "create_product",
51
+ "update_product",
52
+ "delete_product",
53
+ "create_buyer_intent",
54
+ "update_buyer_intent",
55
+ "delete_buyer_intent",
56
+ "recompute_health_scores",
57
+ "refresh_business_overview",
46
58
  }
47
59
 
48
60
  # Tools restricted to pro+ tier
@@ -54,6 +66,7 @@ _PRO_TOOLS: set[str] = {
54
66
  "get_semantic_health",
55
67
  "buyer_fit_query",
56
68
  "get_business_hq",
69
+ "get_agent_brief",
57
70
  "get_analytics_trends",
58
71
  "get_analytics_dashboard",
59
72
  "compare_audits",
@@ -156,6 +169,10 @@ _BASE_TOOLS: set[str] = {
156
169
  "get_business",
157
170
  "crawl_business",
158
171
  "get_health_scores",
172
+ "list_profile_competitors",
173
+ "list_personas",
174
+ "list_products",
175
+ "list_buyer_intents",
159
176
  "list_audit_templates",
160
177
  "get_audit_template",
161
178
  "start_audit",
@@ -204,6 +204,56 @@ class _OAuthEventLogMiddleware:
204
204
  logger.info("[oauth-event] %s", kv)
205
205
 
206
206
 
207
+ class _RequestContextMiddleware:
208
+ """ASGI middleware that captures the request's source IP and User-Agent
209
+ into contextvars consumed by the tool-call decorator.
210
+
211
+ Why: ``tool_call`` log lines previously carried no source-identifying
212
+ metadata beyond the (possibly absent) decoded user JWT. When a probe
213
+ came in with a non-standard token — logged as user=``unknown`` — there
214
+ was no way to trace the source. Threading an HTTP request object
215
+ through every tool function is invasive; contextvars stay scoped to
216
+ the request's async task without touching tool signatures.
217
+
218
+ For stdio transport this middleware is not in the chain, so the
219
+ contextvars stay None and the log fields render as null.
220
+
221
+ We prefer the leftmost value of ``X-Forwarded-For`` when present
222
+ because the ALB inserts the original client IP there and the immediate
223
+ ``scope["client"]`` is the ALB's internal address. Trusting X-F-F is
224
+ only safe behind a known proxy — which is the deploy topology here.
225
+ """
226
+
227
+ def __init__(self, app: ASGIApp) -> None:
228
+ self.app = app
229
+
230
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
231
+ if scope["type"] != "http":
232
+ await self.app(scope, receive, send)
233
+ return
234
+
235
+ from cited_mcp.tools._helpers import _request_ip, _request_ua
236
+
237
+ headers = {k: v for k, v in scope.get("headers", [])}
238
+ xff = headers.get(b"x-forwarded-for", b"").decode("latin-1", errors="replace")
239
+ if xff:
240
+ ip: str | None = xff.split(",")[0].strip() or None
241
+ else:
242
+ client = scope.get("client") or ("?", 0)
243
+ ip = client[0] if client and client[0] != "?" else None
244
+
245
+ ua_bytes = headers.get(b"user-agent", b"")
246
+ ua = ua_bytes.decode("latin-1", errors="replace")[:120] if ua_bytes else None
247
+
248
+ token_ip = _request_ip.set(ip)
249
+ token_ua = _request_ua.set(ua)
250
+ try:
251
+ await self.app(scope, receive, send)
252
+ finally:
253
+ _request_ip.reset(token_ip)
254
+ _request_ua.reset(token_ua)
255
+
256
+
207
257
  @asynccontextmanager
208
258
  async def cited_remote_lifespan(server: FastMCP) -> AsyncIterator[CitedContext]:
209
259
  """Lifespan for the remote MCP server.
@@ -403,9 +453,13 @@ def run_remote_server() -> None:
403
453
  starlette_app = server.streamable_http_app()
404
454
  # Order: oauth-event log on the OUTSIDE of the registration patcher so
405
455
  # patcher-rejected /register calls (400 invalid_redirect_uri) still
406
- # produce an event line.
456
+ # produce an event line. RequestContextMiddleware sits innermost so
457
+ # its contextvars (ip, user_agent) are set in the same task that
458
+ # dispatches the tool call.
407
459
  patched_app = _OAuthEventLogMiddleware(
408
- _PatchRegistrationMiddleware(starlette_app)
460
+ _PatchRegistrationMiddleware(
461
+ _RequestContextMiddleware(starlette_app)
462
+ )
409
463
  )
410
464
 
411
465
  config = uvicorn.Config(
@@ -21,6 +21,176 @@
21
21
  # this file.
22
22
 
23
23
  versions:
24
+ - version: "0.5.1"
25
+ released: "2026-05-18"
26
+ # Telemetry-only release: tool surface is unchanged (fingerprint matches
27
+ # 0.5.0). Adds forensic fields to every tool_call log line — ip,
28
+ # user_agent, and a new auth_kind discriminator (anonymous / agent_key /
29
+ # oauth_email / oauth_no_email / oauth_malformed / stdio_email /
30
+ # stdio_no_email / stdio_malformed). Also fixes client_id extraction
31
+ # which was reading from user_jwt claims (always null in prod) instead
32
+ # of the OAuth access token. Motivated by a 2026-05-02 dev probe that
33
+ # logged user=unknown with no traceable source.
34
+ fingerprint: "5bef45100c53"
35
+
36
+ - version: "0.5.0"
37
+ released: "2026-05-11"
38
+ # Three new read-only list_* tools to enable persona/product/buyer-intent
39
+ # CRUD flows (agents need IDs before they can call update/delete), plus a
40
+ # significant parameter-schema expansion on update_business / create_business
41
+ # to cover the full canonical BusinessUpdate/CreateRequest field set. Also
42
+ # fixes a latent bug: update_business previously sent HTTP PUT against a
43
+ # backend PATCH route. Bumps minor (not patch) because update_business /
44
+ # create_business signature changes shift parameter ordering and clients
45
+ # diffing the fingerprint should pick this up as non-additive.
46
+ fingerprint: "5bef45100c53"
47
+ tools_added:
48
+ - name: list_personas
49
+ description: |
50
+ List buyer personas declared on the business profile. Required
51
+ before update_persona / delete_persona since the agent has no
52
+ other way to discover persona_ids.
53
+ - name: list_products
54
+ description: |
55
+ List products / service offerings declared on the business
56
+ profile. Required before update_product / delete_product to
57
+ discover product_ids.
58
+ - name: list_buyer_intents
59
+ description: |
60
+ List buyer-intent entries declared on the business profile.
61
+ Use before recommending a new create_buyer_intent to avoid
62
+ overlapping entries.
63
+ - name: update_buyer_intent
64
+ description: |
65
+ Update a buyer-intent entry (question, intent_type, priority,
66
+ is_answered, answering_page_url — all optional). Closes the
67
+ persona/product CRUD symmetry. Requires the cited backend with
68
+ PATCH /businesses/{id}/buyer-intents/{intent_id} (added in
69
+ companion backend PR).
70
+ - name: delete_buyer_intent
71
+ description: |
72
+ Delete a buyer intent. Confirm with the user before calling —
73
+ there's no undo. Requires the cited backend with
74
+ DELETE /businesses/{id}/buyer-intents/{intent_id}.
75
+ tools_changed:
76
+ - name: update_business
77
+ change_summary: |
78
+ Parameter set expanded from 4 fields (name, website, description,
79
+ industry) to the full BusinessUpdateRequest schema: adds
80
+ one_line_description, primary_customer, problem_solved,
81
+ offering_type, primary_market, service_areas,
82
+ headquarters_country, goals, discovery_channels,
83
+ success_definition, plus primary-location fields (address, city,
84
+ region, country). All new params are optional and keyword-only.
85
+ Also fixes a method bug: now sends PATCH (was PUT) to match the
86
+ backend @router.patch route.
87
+ - name: create_business
88
+ change_summary: |
89
+ Parameter set expanded to the full BusinessCreateRequest schema:
90
+ description is now optional (server composes from structured
91
+ fields if omitted); adds one_line_description, primary_customer,
92
+ problem_solved, offering_type, primary_market, service_areas,
93
+ headquarters_country, goals, discovery_channels,
94
+ success_definition. Existing 4-field callers unaffected
95
+ (description was already accepted; making it optional is
96
+ backward-compatible).
97
+
98
+ - version: "0.4.0"
99
+ released: "2026-05-08"
100
+ # 12 new tools + docstring/parameter-schema edits on 4 existing tools —
101
+ # significant surface expansion, so the minor version bumps to 0.4.0
102
+ # to signal a non-additive-only release for clients that diff the
103
+ # fingerprint.
104
+ fingerprint: "8197f97dcd06"
105
+ tools_added:
106
+ - name: get_agent_brief
107
+ description: |
108
+ Single-call composite "what should I focus on right now?" —
109
+ top priority actions, quick wins, failing deterministic checks,
110
+ and recent citation-rate movement. Designed to replace
111
+ chained get_business_hq + get_quick_wins + check_status calls
112
+ in opening-turn agent flows.
113
+ - name: list_profile_competitors
114
+ description: |
115
+ List the user-declared competitors on a business profile.
116
+ These bypass third-party domain filtering and rank ahead of
117
+ citation-only picks in head-to-head comparisons.
118
+ - name: set_profile_competitors
119
+ description: |
120
+ Replace the full list of profile competitors (max 10).
121
+ Pass [{name, website}] entries; PUT replaces. Use when the
122
+ user reports an obviously-relevant rival missing from H2H.
123
+ - name: recompute_health_scores
124
+ description: |
125
+ Force a fresh recomputation of business health scores after
126
+ profile/HQ edits — no crawl, no audit, just re-derive from
127
+ existing data.
128
+ - name: refresh_business_overview
129
+ description: |
130
+ Refresh cached audit / recommendation overview projections
131
+ for a business. scope ∈ {audit, recommendations, all}.
132
+ - name: create_persona
133
+ description: Create a target buyer persona on a business.
134
+ - name: update_persona
135
+ description: Update a persona (partial — only changed fields).
136
+ - name: delete_persona
137
+ description: Delete a persona.
138
+ - name: create_product
139
+ description: Create a product/service offering on a business.
140
+ - name: update_product
141
+ description: Update a product (partial).
142
+ - name: delete_product
143
+ description: Delete a product.
144
+ - name: create_buyer_intent
145
+ description: |
146
+ Create a buyer-intent entry tying personas to products
147
+ through specific decision moments — drives audit question
148
+ generation and per-intent coverage scoring.
149
+ tools_changed:
150
+ - name: get_business_hq
151
+ change_summary: |
152
+ New include flags ``include_agentic_readiness`` and
153
+ ``include_trust_signals`` add the agentic-readiness summary
154
+ and trust-signal evidence to the dashboard payload. Existing
155
+ callers are unaffected — both default to false.
156
+ - name: get_competitive_comparison
157
+ change_summary: |
158
+ Docstring now describes the new ``analysis_status`` and
159
+ ``last_analysis_run_at`` freshness fields on the response
160
+ (added in cited backend commit 2c716be). Tool behavior is
161
+ unchanged — these were already passed through; the docstring
162
+ was previously silent about them.
163
+ - name: get_recommendation_check_status
164
+ change_summary: |
165
+ Updated for Validation Engine Tier 4: now runs 72
166
+ deterministic checks (was 62). New checks fill
167
+ monitoring_adaptation and original_research domains
168
+ (knowledge_graph_linkage, aggregate_rating_present,
169
+ author_bio_on_blog_posts, sitemap_lastmod_freshness,
170
+ outbound_authority_citations, hreflang_and_lang_consistency,
171
+ well_known_agent_discovery). Response shape unchanged.
172
+ - name: crawl_business
173
+ change_summary: |
174
+ Docstring now notes the post-crawl auto-fill side effect:
175
+ empty business-profile fields (description, founding year,
176
+ NAICS, sameAs links) are populated from JSON-LD / meta tags
177
+ found on the homepage. User-supplied values are never
178
+ overwritten. (cited backend CIT-148.)
179
+ tools_removed: []
180
+ behavior_changed:
181
+ - area: plan_gating
182
+ change_summary: |
183
+ Tier assignments for the new tools:
184
+ - base (all tiers): ``list_profile_competitors``
185
+ - scale: ``set_profile_competitors``,
186
+ ``create/update/delete_persona``,
187
+ ``create/update/delete_product``,
188
+ ``create_buyer_intent``,
189
+ ``recompute_health_scores``,
190
+ ``refresh_business_overview``
191
+ - pro: ``get_agent_brief`` (matches the ``get_business_hq``
192
+ tier — same composite-dashboard semantics).
193
+
24
194
  - version: "0.3.15"
25
195
  released: "2026-05-05"
26
196
  # Fingerprint unchanged from 0.3.14 image runtime — pure observability +
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextlib
4
+ import contextvars
4
5
  import functools
5
6
  import hashlib
6
7
  import json
@@ -22,6 +23,20 @@ from cited_mcp.context import CitedContext
22
23
 
23
24
  logger = logging.getLogger("cited_mcp.usage")
24
25
 
26
+ # ---------------------------------------------------------------------------
27
+ # Per-request forensic context (set by remote.py _RequestContextMiddleware).
28
+ #
29
+ # These let tool_call log lines carry the request's source IP and User-Agent
30
+ # without threading an HTTP request object through every tool function. For
31
+ # stdio transport (no HTTP) they remain None and are emitted as null in logs.
32
+ # ---------------------------------------------------------------------------
33
+ _request_ip: contextvars.ContextVar[str | None] = contextvars.ContextVar(
34
+ "cited_mcp_request_ip", default=None
35
+ )
36
+ _request_ua: contextvars.ContextVar[str | None] = contextvars.ContextVar(
37
+ "cited_mcp_request_ua", default=None
38
+ )
39
+
25
40
  # ---------------------------------------------------------------------------
26
41
  # Response size guardrails
27
42
  # ---------------------------------------------------------------------------
@@ -244,18 +259,84 @@ def _extract_user(token: str | None) -> str:
244
259
  return "unknown"
245
260
 
246
261
 
247
- def _extract_client_id(token: str | None) -> str | None:
248
- """Decode OAuth client_id from a JWT (for connector-mix analytics)."""
249
- if not token:
262
+ def _extract_client_id_from_oauth() -> str | None:
263
+ """Return the OAuth client_id from the current access token, truncated.
264
+
265
+ The MCP server's client_id is itself a signed JWT (RFC 7591 dynamic
266
+ registration encoded into the id), so we truncate to a 12-char prefix
267
+ sufficient to group connector traffic without bloating log lines.
268
+
269
+ Returns None for stdio transport (no OAuth context), or whenever the
270
+ access-token contextvar is unset.
271
+
272
+ Note: this used to decode the user_jwt looking for client_id/aud/azp
273
+ claims, but backend session JWTs do not carry OAuth-client identity —
274
+ so the field was always empty in production logs. This implementation
275
+ reads from the OAuth access token instead.
276
+ """
277
+ try:
278
+ from mcp.server.auth.middleware.auth_context import get_access_token
279
+ except ImportError:
250
280
  return None
251
281
  try:
252
- payload = pyjwt.decode(token, options={"verify_signature": False})
253
- cid = payload.get("client_id") or payload.get("aud") or payload.get("azp")
254
- if isinstance(cid, list):
255
- cid = cid[0] if cid else None
256
- return str(cid) if cid else None
282
+ access_token = get_access_token()
257
283
  except Exception:
258
284
  return None
285
+ if not access_token:
286
+ return None
287
+ cid = getattr(access_token, "client_id", None)
288
+ if not cid:
289
+ return None
290
+ return str(cid)[:12]
291
+
292
+
293
+ def _extract_auth_kind(
294
+ ctx: Context[Any, CitedContext, Any], token: str | None
295
+ ) -> str:
296
+ """Categorize the auth path for forensic logging.
297
+
298
+ Returns one of:
299
+ - ``anonymous`` — no token, no agent key
300
+ - ``agent_key`` — CitedClient has agent_api_key set (stdio mode)
301
+ - ``oauth_email`` — OAuth flow, user_jwt has email claim
302
+ - ``oauth_no_email`` — OAuth flow, user_jwt decoded but no email
303
+ - ``oauth_malformed`` — OAuth flow, user_jwt couldn't be decoded
304
+ - ``stdio_email`` — stdio mode, token is a JWT with email claim
305
+ - ``stdio_no_email`` — stdio mode, token decoded but no email
306
+ - ``stdio_malformed`` — stdio mode, token isn't a parseable JWT
307
+
308
+ Lets us tell apart the practical cases — including the case where
309
+ ``_extract_user`` returns ``"unknown"`` because the token was not a
310
+ JWT at all versus because the JWT had no email/sub claim. Without
311
+ this, the ``<unknown>`` bucket in usage reports is unactionable.
312
+ """
313
+ try:
314
+ lc = ctx.request_context.lifespan_context
315
+ if getattr(lc.client, "agent_api_key", None):
316
+ return "agent_key"
317
+ except Exception:
318
+ pass
319
+
320
+ method = "stdio"
321
+ try:
322
+ from mcp.server.auth.middleware.auth_context import get_access_token
323
+
324
+ if get_access_token() is not None:
325
+ method = "oauth"
326
+ except Exception:
327
+ pass
328
+
329
+ if not token:
330
+ return "anonymous" if method == "stdio" else "oauth_no_token"
331
+
332
+ try:
333
+ payload = pyjwt.decode(token, options={"verify_signature": False})
334
+ except Exception:
335
+ return f"{method}_malformed"
336
+
337
+ if payload.get("email"):
338
+ return f"{method}_email"
339
+ return f"{method}_no_email"
259
340
 
260
341
 
261
342
  # ---------------------------------------------------------------------------
@@ -384,7 +465,8 @@ def log_tool_call(
384
465
  token = _resolve_token(ctx)
385
466
  rl_key = _rate_limit_key(token)
386
467
  user = _extract_user(token)
387
- client_id = _extract_client_id(token)
468
+ auth_kind = _extract_auth_kind(ctx, token)
469
+ client_id = _extract_client_id_from_oauth()
388
470
  request_id = uuid.uuid4().hex[:12]
389
471
  start = time.monotonic()
390
472
  build = _get_build_identity()
@@ -399,7 +481,10 @@ def log_tool_call(
399
481
  "request_id": request_id,
400
482
  "tool": tool_name,
401
483
  "user": user,
484
+ "auth_kind": auth_kind,
402
485
  "client_id": client_id,
486
+ "ip": _request_ip.get(),
487
+ "user_agent": _request_ua.get(),
403
488
  "server_version": build["server_version"],
404
489
  "tools_fingerprint": build["tools_fingerprint"],
405
490
  "deployment_id": build["deployment_id"],
@@ -407,7 +492,15 @@ def log_tool_call(
407
492
  if resources:
408
493
  payload["resources"] = resources
409
494
  payload.update(extra)
410
- logger.info(json.dumps(payload, default=str))
495
+ # CodeQL flags this as clear-text logging of sensitive data because
496
+ # `user` is sourced from a JWT email claim. This is by design —
497
+ # operator usage reports (scripts/mcp_usage_report.py) attribute
498
+ # tool calls to users by email, which is necessary for forensics
499
+ # and abuse investigation. Logs flow to a private CloudWatch group
500
+ # with restricted IAM access. Email is not transmitted anywhere
501
+ # else from this line. If we later move to hashed identifiers,
502
+ # the suppression can be removed.
503
+ logger.info(json.dumps(payload, default=str)) # noqa: E501 lgtm[py/clear-text-logging-sensitive-data]
411
504
 
412
505
  # Rate limit check
413
506
  if rl_err := _check_rate_limit(rl_key):