cited-cli 0.3.2__py3-none-any.whl

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 (48) hide show
  1. cited_cli/__init__.py +1 -0
  2. cited_cli/__main__.py +3 -0
  3. cited_cli/api/__init__.py +0 -0
  4. cited_cli/api/client.py +111 -0
  5. cited_cli/api/endpoints.py +88 -0
  6. cited_cli/app.py +199 -0
  7. cited_cli/auth/__init__.py +0 -0
  8. cited_cli/auth/oauth_server.py +123 -0
  9. cited_cli/auth/store.py +83 -0
  10. cited_cli/commands/__init__.py +0 -0
  11. cited_cli/commands/agent.py +129 -0
  12. cited_cli/commands/analytics.py +83 -0
  13. cited_cli/commands/audit.py +177 -0
  14. cited_cli/commands/auth.py +353 -0
  15. cited_cli/commands/business.py +217 -0
  16. cited_cli/commands/config_cmd.py +111 -0
  17. cited_cli/commands/hq.py +125 -0
  18. cited_cli/commands/job.py +106 -0
  19. cited_cli/commands/mcp_cmd.py +30 -0
  20. cited_cli/commands/named_audit.py +244 -0
  21. cited_cli/commands/recommend.py +211 -0
  22. cited_cli/commands/solution.py +154 -0
  23. cited_cli/config/__init__.py +0 -0
  24. cited_cli/config/constants.py +48 -0
  25. cited_cli/config/manager.py +69 -0
  26. cited_cli/mcp/__init__.py +1 -0
  27. cited_cli/mcp/context.py +12 -0
  28. cited_cli/mcp/server.py +54 -0
  29. cited_cli/mcp/tools/__init__.py +1 -0
  30. cited_cli/mcp/tools/_helpers.py +33 -0
  31. cited_cli/mcp/tools/audit.py +129 -0
  32. cited_cli/mcp/tools/auth.py +23 -0
  33. cited_cli/mcp/tools/business.py +96 -0
  34. cited_cli/mcp/tools/job.py +57 -0
  35. cited_cli/mcp/tools/recommend.py +96 -0
  36. cited_cli/mcp/tools/solution.py +66 -0
  37. cited_cli/output/__init__.py +0 -0
  38. cited_cli/output/formatter.py +61 -0
  39. cited_cli/output/progress.py +56 -0
  40. cited_cli/output/tables.py +49 -0
  41. cited_cli/utils/__init__.py +0 -0
  42. cited_cli/utils/errors.py +31 -0
  43. cited_cli/utils/interactive.py +83 -0
  44. cited_cli-0.3.2.dist-info/METADATA +515 -0
  45. cited_cli-0.3.2.dist-info/RECORD +48 -0
  46. cited_cli-0.3.2.dist-info/WHEEL +4 -0
  47. cited_cli-0.3.2.dist-info/entry_points.txt +2 -0
  48. cited_cli-0.3.2.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Any
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cited_cli.output.formatter import OutputContext, print_error, print_result
9
+ from cited_cli.output.tables import render_kv, render_table
10
+ from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
11
+ from cited_cli.utils.interactive import prompt_if_missing
12
+ from cited_core.api import endpoints
13
+ from cited_core.api.client import LONG_TIMEOUT, CitedClient
14
+ from cited_core.auth.store import TokenStore
15
+ from cited_core.config.manager import ConfigManager
16
+
17
+ recommend_app = typer.Typer(name="recommend", help="Generate and manage recommendations.")
18
+
19
+
20
+ def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
21
+ obj = ctx.obj or {}
22
+ out: OutputContext = obj.get("output", OutputContext())
23
+ cfg: ConfigManager = obj.get("config", ConfigManager())
24
+ profile = obj.get("profile", "default")
25
+ env = cfg.get_environment(profile, obj.get("env_override"))
26
+ api_url = cfg.get_api_url(profile, obj.get("env_override"))
27
+
28
+ store = TokenStore()
29
+ token = store.get_token(env)
30
+ if not token:
31
+ print_error(f"Not logged in to {env}. Run: cited login", out)
32
+ raise typer.Exit(ExitCode.AUTH_ERROR)
33
+
34
+ return out, CitedClient(base_url=api_url, token=token)
35
+
36
+
37
+ @recommend_app.command("start")
38
+ def recommend_start(
39
+ ctx: typer.Context,
40
+ audit_job_id: Annotated[
41
+ str | None,
42
+ typer.Argument(help="Audit job ID to generate recommendations from"),
43
+ ] = None,
44
+ ) -> None:
45
+ """Generate recommendations from an audit."""
46
+ out, client = _get_client(ctx)
47
+ audit_job_id = prompt_if_missing(audit_job_id, "AUDIT_JOB_ID", "Audit job ID", out)
48
+ try:
49
+ data = client.post(
50
+ endpoints.RECOMMEND_START,
51
+ json={"audit_job_id": audit_job_id},
52
+ timeout=LONG_TIMEOUT,
53
+ )
54
+ job_id = data.get("job_id", data.get("id", ""))
55
+ print_result(
56
+ data,
57
+ out,
58
+ human_formatter=lambda d, c: render_kv("Recommendations Started", d, c),
59
+ )
60
+ if not out.json_mode and job_id:
61
+ out.console.print(f"\nTrack progress: [bold]cited job watch {job_id}[/bold]")
62
+ except CitedAPIError as e:
63
+ handle_api_error(e, out.json_mode)
64
+ finally:
65
+ client.close()
66
+
67
+
68
+ @recommend_app.command("status")
69
+ def recommend_status(
70
+ ctx: typer.Context,
71
+ job_id: Annotated[str, typer.Argument(help="Recommendation job ID")],
72
+ ) -> None:
73
+ """Check recommendation job status."""
74
+ out, client = _get_client(ctx)
75
+ try:
76
+ path = endpoints.RECOMMEND_STATUS.format(job_id=job_id)
77
+ data = client.get(path)
78
+ print_result(
79
+ data,
80
+ out,
81
+ human_formatter=lambda d, c: render_kv("Recommendation Status", d, c),
82
+ )
83
+ except CitedAPIError as e:
84
+ handle_api_error(e, out.json_mode)
85
+ finally:
86
+ client.close()
87
+
88
+
89
+ @recommend_app.command("result")
90
+ def recommend_result(
91
+ ctx: typer.Context,
92
+ job_id: Annotated[str, typer.Argument(help="Recommendation job ID")],
93
+ ) -> None:
94
+ """Get recommendation results."""
95
+ out, client = _get_client(ctx)
96
+ try:
97
+ path = endpoints.RECOMMEND_RESULT.format(job_id=job_id)
98
+ data = client.get(path)
99
+ print_result(data, out)
100
+ except CitedAPIError as e:
101
+ handle_api_error(e, out.json_mode)
102
+ finally:
103
+ client.close()
104
+
105
+
106
+ @recommend_app.command("insights")
107
+ def recommend_insights(
108
+ ctx: typer.Context,
109
+ job_id: Annotated[str, typer.Argument(help="Recommendation job ID")],
110
+ ) -> None:
111
+ """List solution-ready insights from a recommendation job."""
112
+ out, client = _get_client(ctx)
113
+ try:
114
+ path = endpoints.RECOMMEND_RESULT.format(job_id=job_id)
115
+ data = client.get(path)
116
+
117
+ def _human(d: object, console: Console) -> None:
118
+ if not isinstance(d, dict):
119
+ console.print(d)
120
+ return
121
+ data: dict[str, Any] = d
122
+ rows: list[list[object]] = []
123
+
124
+ # question_insights: id=question_id, desc=question_text
125
+ for item in data.get("question_insights", []):
126
+ if not isinstance(item, dict):
127
+ continue
128
+ source_id = item.get("question_id", item.get("id", ""))
129
+ desc = str(item.get("question_text", item.get("question", "")))
130
+ rows.append([str(len(rows) + 1), "question_insight", source_id, desc[:60]])
131
+
132
+ # head_to_head_comparisons: id=competitor_domain
133
+ for item in data.get("head_to_head_comparisons", data.get("head_to_head", [])):
134
+ if not isinstance(item, dict):
135
+ continue
136
+ source_id = item.get("competitor_domain", item.get("id", ""))
137
+ desc = str(item.get("competitor_domain", item.get("description", "")))
138
+ rows.append([str(len(rows) + 1), "head_to_head", source_id, desc[:60]])
139
+
140
+ # strengthening_tips: id=category (no uuid), desc=title
141
+ for item in data.get("strengthening_tips", []):
142
+ if not isinstance(item, dict):
143
+ continue
144
+ source_id = item.get("category", item.get("id", item.get("source_id", "")))
145
+ desc = str(item.get("title", item.get("description", "")))
146
+ rows.append([str(len(rows) + 1), "strengthening_tip", source_id, desc[:60]])
147
+
148
+ # priority_actions: id=id or category
149
+ for item in data.get("priority_actions", []):
150
+ if not isinstance(item, dict):
151
+ continue
152
+ source_id = item.get("id", item.get("category", item.get("source_id", "")))
153
+ desc = str(item.get("title", item.get("description", item.get("action", ""))))
154
+ rows.append([str(len(rows) + 1), "priority_action", source_id, desc[:60]])
155
+
156
+ render_table(
157
+ "Available Insights",
158
+ ["#", "Type", "Source ID", "Description"],
159
+ rows,
160
+ console,
161
+ )
162
+ if rows:
163
+ console.print(
164
+ "\nRun a solution: [bold]cited solution start "
165
+ f"{job_id} --type <type> --source <source_id>[/bold]"
166
+ )
167
+
168
+ print_result(data, out, human_formatter=_human)
169
+ except CitedAPIError as e:
170
+ handle_api_error(e, out.json_mode)
171
+ finally:
172
+ client.close()
173
+
174
+
175
+ @recommend_app.command("list")
176
+ def recommend_list(
177
+ ctx: typer.Context,
178
+ audit_job_id: Annotated[
179
+ str | None,
180
+ typer.Option("--audit", "-a", help="Filter by audit job ID"),
181
+ ] = None,
182
+ ) -> None:
183
+ """List recommendation history."""
184
+ out, client = _get_client(ctx)
185
+ try:
186
+ if audit_job_id:
187
+ path = endpoints.RECOMMEND_HISTORY.format(audit_job_id=audit_job_id)
188
+ data = client.get(path)
189
+ else:
190
+ data = client.get("/recommendations/history/bulk")
191
+ recs = (
192
+ data if isinstance(data, list)
193
+ else data.get("recommendations", data.get("items", []))
194
+ )
195
+
196
+ def _human(d: object, console: Console) -> None:
197
+ items = d if isinstance(d, list) else []
198
+ rows = []
199
+ for r in items:
200
+ rows.append([
201
+ r.get("job_id", r.get("id", ""))[:8],
202
+ r.get("status", ""),
203
+ r.get("created_at", "")[:19] if r.get("created_at") else "",
204
+ ])
205
+ render_table("Recommendations", ["Job ID", "Status", "Created"], rows, console)
206
+
207
+ print_result(recs, out, human_formatter=_human)
208
+ except CitedAPIError as e:
209
+ handle_api_error(e, out.json_mode)
210
+ finally:
211
+ client.close()
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cited_cli.output.formatter import OutputContext, print_error, print_result
9
+ from cited_cli.output.tables import render_kv, render_table
10
+ from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
11
+ from cited_cli.utils.interactive import prompt_choice, prompt_if_missing
12
+ from cited_core.api import endpoints
13
+ from cited_core.api.client import LONG_TIMEOUT, CitedClient
14
+ from cited_core.auth.store import TokenStore
15
+ from cited_core.config.constants import VALID_SOURCE_TYPES
16
+ from cited_core.config.manager import ConfigManager
17
+
18
+ solution_app = typer.Typer(name="solution", help="Generate and manage content solutions.")
19
+
20
+
21
+ def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient, str]:
22
+ obj = ctx.obj or {}
23
+ out: OutputContext = obj.get("output", OutputContext())
24
+ cfg: ConfigManager = obj.get("config", ConfigManager())
25
+ profile = obj.get("profile", "default")
26
+ env = cfg.get_environment(profile, obj.get("env_override"))
27
+ api_url = cfg.get_api_url(profile, obj.get("env_override"))
28
+
29
+ store = TokenStore()
30
+ token = store.get_token(env)
31
+ if not token:
32
+ print_error(f"Not logged in to {env}. Run: cited login", out)
33
+ raise typer.Exit(ExitCode.AUTH_ERROR)
34
+
35
+ return out, CitedClient(base_url=api_url, token=token), env
36
+
37
+
38
+ @solution_app.command("start")
39
+ def solution_start(
40
+ ctx: typer.Context,
41
+ recommendation_job_id: Annotated[
42
+ str | None,
43
+ typer.Argument(help="Recommendation job ID"),
44
+ ] = None,
45
+ source_type: Annotated[
46
+ str | None,
47
+ typer.Option(
48
+ "--type", "-t",
49
+ help="Source type: question_insight, head_to_head, strengthening_tip, priority_action",
50
+ ),
51
+ ] = None,
52
+ source_id: Annotated[
53
+ str | None,
54
+ typer.Option("--source", "-s", help="Source ID from 'cited recommend insights'"),
55
+ ] = None,
56
+ ) -> None:
57
+ """Generate a content solution from a recommendation insight."""
58
+ out, client, env = _get_client(ctx)
59
+ recommendation_job_id = prompt_if_missing(
60
+ recommendation_job_id, "RECOMMENDATION_JOB_ID", "Recommendation job ID", out
61
+ )
62
+ source_type = prompt_choice(
63
+ source_type, "--type", "Select source type:", VALID_SOURCE_TYPES, out
64
+ )
65
+ source_id = prompt_if_missing(source_id, "--source", "Source ID", out)
66
+ try:
67
+ data = client.post(
68
+ endpoints.SOLUTION_REQUEST,
69
+ json={
70
+ "recommendation_job_id": recommendation_job_id,
71
+ "source_type": source_type,
72
+ "source_id": source_id,
73
+ },
74
+ timeout=LONG_TIMEOUT,
75
+ )
76
+ job_id = data.get("job_id", data.get("id", ""))
77
+ print_result(data, out, human_formatter=lambda d, c: render_kv("Solution Started", d, c))
78
+ if not out.json_mode and job_id:
79
+ out.console.print(f"\nTrack progress: [bold]cited job watch {job_id}[/bold]")
80
+ web_base = f"https://{env}.youcited.com" if env != "prod" else "https://youcited.com"
81
+ out.console.print(f"View artifacts: [bold]{web_base}/solutions/{job_id}[/bold]")
82
+ except CitedAPIError as e:
83
+ handle_api_error(e, out.json_mode)
84
+ finally:
85
+ client.close()
86
+
87
+
88
+ @solution_app.command("status")
89
+ def solution_status(
90
+ ctx: typer.Context,
91
+ job_id: Annotated[str, typer.Argument(help="Solution job ID")],
92
+ ) -> None:
93
+ """Check solution job status."""
94
+ out, client, _ = _get_client(ctx)
95
+ try:
96
+ path = endpoints.SOLUTION_STATUS.format(job_id=job_id)
97
+ data = client.get(path)
98
+ print_result(data, out, human_formatter=lambda d, c: render_kv("Solution Status", d, c))
99
+ except CitedAPIError as e:
100
+ handle_api_error(e, out.json_mode)
101
+ finally:
102
+ client.close()
103
+
104
+
105
+ @solution_app.command("result")
106
+ def solution_result(
107
+ ctx: typer.Context,
108
+ job_id: Annotated[str, typer.Argument(help="Solution job ID")],
109
+ ) -> None:
110
+ """Get solution results."""
111
+ out, client, _ = _get_client(ctx)
112
+ try:
113
+ path = endpoints.SOLUTION_RESULT.format(job_id=job_id)
114
+ data = client.get(path)
115
+ print_result(data, out)
116
+ except CitedAPIError as e:
117
+ handle_api_error(e, out.json_mode)
118
+ finally:
119
+ client.close()
120
+
121
+
122
+ @solution_app.command("list")
123
+ def solution_list(
124
+ ctx: typer.Context,
125
+ business_id: Annotated[
126
+ str | None,
127
+ typer.Option("--business", "-b", help="Filter by business ID"),
128
+ ] = None,
129
+ ) -> None:
130
+ """List solution history."""
131
+ out, client, _ = _get_client(ctx)
132
+ try:
133
+ params = {}
134
+ if business_id:
135
+ params["business_id"] = business_id
136
+ data = client.get(endpoints.SOLUTION_HISTORY, params=params or None)
137
+ solutions = data if isinstance(data, list) else data.get("solutions", data.get("items", []))
138
+
139
+ def _human(d: object, console: Console) -> None:
140
+ items = d if isinstance(d, list) else []
141
+ rows = []
142
+ for s in items:
143
+ rows.append([
144
+ s.get("job_id", s.get("id", ""))[:8],
145
+ s.get("status", ""),
146
+ s.get("created_at", "")[:19] if s.get("created_at") else "",
147
+ ])
148
+ render_table("Solutions", ["Job ID", "Status", "Created"], rows, console)
149
+
150
+ print_result(solutions, out, human_formatter=_human)
151
+ except CitedAPIError as e:
152
+ handle_api_error(e, out.json_mode)
153
+ finally:
154
+ client.close()
File without changes
@@ -0,0 +1,48 @@
1
+ from pathlib import Path
2
+
3
+ ENVIRONMENTS: dict[str, str] = {
4
+ "prod": "https://api.youcited.com",
5
+ "dev": "https://dev.youcited.com",
6
+ "local": "http://localhost:8000",
7
+ }
8
+
9
+ FRONTEND_URLS: dict[str, str] = {
10
+ "prod": "https://app.youcited.com",
11
+ "dev": "https://dev.youcited.com",
12
+ "local": "http://localhost:3000",
13
+ }
14
+
15
+ DEFAULT_ENV = "prod"
16
+ CONFIG_DIR = Path.home() / ".cited"
17
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
18
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
19
+ KEYRING_SERVICE = "cited-cli"
20
+
21
+ VALID_INDUSTRIES = [
22
+ "automotive",
23
+ "beauty",
24
+ "consulting",
25
+ "education",
26
+ "entertainment",
27
+ "finance",
28
+ "fitness",
29
+ "government",
30
+ "healthcare",
31
+ "home_services",
32
+ "hospitality",
33
+ "legal",
34
+ "manufacturing",
35
+ "non_profit",
36
+ "real_estate",
37
+ "restaurant",
38
+ "retail",
39
+ "technology",
40
+ "other",
41
+ ]
42
+
43
+ VALID_SOURCE_TYPES = [
44
+ "question_insight",
45
+ "head_to_head",
46
+ "strengthening_tip",
47
+ "priority_action",
48
+ ]
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from typing import Any
5
+
6
+ import tomli_w
7
+
8
+ from cited_cli.config.constants import CONFIG_DIR, CONFIG_FILE, DEFAULT_ENV, ENVIRONMENTS
9
+
10
+ VALID_KEYS = {
11
+ "environment",
12
+ "default_business_id",
13
+ "agent_api_key",
14
+ "output",
15
+ }
16
+
17
+ VALID_OUTPUT_VALUES = {"json", "text"}
18
+
19
+
20
+ class ConfigManager:
21
+ def __init__(self) -> None:
22
+ self._ensure_config_dir()
23
+ self._data: dict[str, Any] = self._load()
24
+
25
+ def _ensure_config_dir(self) -> None:
26
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
27
+
28
+ def _load(self) -> dict[str, Any]:
29
+ if not CONFIG_FILE.exists():
30
+ return {"default": {}}
31
+ with open(CONFIG_FILE, "rb") as f:
32
+ return tomllib.load(f)
33
+
34
+ def _save(self) -> None:
35
+ with open(CONFIG_FILE, "wb") as f:
36
+ tomli_w.dump(self._data, f)
37
+
38
+ def get(self, key: str, profile: str = "default") -> Any:
39
+ section = self._data.get(profile, self._data.get("default", {}))
40
+ return section.get(key)
41
+
42
+ def set(self, key: str, value: str, profile: str = "default") -> None:
43
+ if profile not in self._data:
44
+ self._data[profile] = {}
45
+ self._data[profile][key] = value
46
+ self._save()
47
+
48
+ def delete(self, key: str, profile: str = "default") -> bool:
49
+ section = self._data.get(profile, {})
50
+ if key in section:
51
+ del section[key]
52
+ self._save()
53
+ return True
54
+ return False
55
+
56
+ def get_all(self, profile: str = "default") -> dict[str, Any]:
57
+ return dict(self._data.get(profile, {}))
58
+
59
+ def get_environment(self, profile: str = "default", override: str | None = None) -> str:
60
+ if override:
61
+ return override
62
+ return self.get("environment", profile) or DEFAULT_ENV
63
+
64
+ def get_api_url(self, profile: str = "default", env_override: str | None = None) -> str:
65
+ env = self.get_environment(profile, env_override)
66
+ return ENVIRONMENTS.get(env, ENVIRONMENTS[DEFAULT_ENV])
67
+
68
+ def get_profiles(self) -> list[str]:
69
+ return [k for k in self._data if k != "default" and isinstance(self._data[k], dict)]
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from cited_cli.api.client import CitedClient
6
+
7
+
8
+ @dataclass
9
+ class CitedContext:
10
+ client: CitedClient
11
+ env: str
12
+ api_url: str
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import AsyncIterator
5
+ from contextlib import asynccontextmanager
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+ from cited_cli.api.client import CitedClient
10
+ from cited_cli.auth.store import TokenStore
11
+ from cited_cli.config.manager import ConfigManager
12
+ from cited_cli.mcp.context import CitedContext
13
+
14
+
15
+ @asynccontextmanager
16
+ async def cited_lifespan(server: FastMCP) -> AsyncIterator[CitedContext]:
17
+ """Set up CitedClient for the MCP server lifetime."""
18
+ config = ConfigManager()
19
+ env_override = os.environ.get("CITED_ENV")
20
+ env = config.get_environment(override=env_override)
21
+ api_url = config.get_api_url(env_override=env_override)
22
+
23
+ token = TokenStore().get_token(env)
24
+ agent_api_key = os.environ.get("CITED_AGENT_API_KEY") or config.get("agent_api_key")
25
+
26
+ client = CitedClient(
27
+ base_url=api_url,
28
+ token=token,
29
+ agent_api_key=agent_api_key,
30
+ )
31
+ try:
32
+ yield CitedContext(client=client, env=env, api_url=api_url)
33
+ finally:
34
+ client.close()
35
+
36
+
37
+ mcp = FastMCP(
38
+ "cited",
39
+ instructions="Cited GEO platform — audit, optimize, and monitor AI search presence",
40
+ lifespan=cited_lifespan,
41
+ )
42
+
43
+ # Import tool modules to trigger @mcp.tool() registrations
44
+ import cited_cli.mcp.tools.auth # noqa: I001, E402, F401
45
+ import cited_cli.mcp.tools.audit # noqa: E402, F401
46
+ import cited_cli.mcp.tools.business # noqa: E402, F401
47
+ import cited_cli.mcp.tools.job # noqa: E402, F401
48
+ import cited_cli.mcp.tools.recommend # noqa: E402, F401
49
+ import cited_cli.mcp.tools.solution # noqa: E402, F401
50
+
51
+
52
+ def run_server() -> None:
53
+ """Start the MCP server on stdio transport."""
54
+ mcp.run(transport="stdio")
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.mcp.context import CitedContext
8
+ from cited_cli.utils.errors import CitedAPIError
9
+
10
+
11
+ def _get_ctx(ctx: Context[Any, CitedContext, Any]) -> CitedContext:
12
+ """Extract CitedContext from MCP lifespan context."""
13
+ lc: CitedContext = ctx.request_context.lifespan_context
14
+ return lc
15
+
16
+
17
+ def _auth_check(cited_ctx: CitedContext) -> dict[str, Any] | None:
18
+ """Return error dict if not authenticated, else None."""
19
+ if cited_ctx.client.token is None:
20
+ return {
21
+ "error": True,
22
+ "message": "Not authenticated. Run 'cited login' first.",
23
+ }
24
+ return None
25
+
26
+
27
+ def _api_error_response(e: CitedAPIError) -> dict[str, Any]:
28
+ """Convert CitedAPIError to a structured error dict."""
29
+ return {
30
+ "error": True,
31
+ "status_code": e.status_code,
32
+ "message": e.message,
33
+ }