cited-mcp 0.3.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_mcp-0.3.2/.gitignore +19 -0
- cited_mcp-0.3.2/PKG-INFO +14 -0
- cited_mcp-0.3.2/pyproject.toml +29 -0
- cited_mcp-0.3.2/src/cited_mcp/__init__.py +4 -0
- cited_mcp-0.3.2/src/cited_mcp/__main__.py +5 -0
- cited_mcp-0.3.2/src/cited_mcp/context.py +12 -0
- cited_mcp-0.3.2/src/cited_mcp/server.py +54 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/__init__.py +1 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/_helpers.py +33 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/audit.py +186 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/auth.py +59 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/business.py +153 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/job.py +57 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/recommend.py +117 -0
- cited_mcp-0.3.2/src/cited_mcp/tools/solution.py +78 -0
cited_mcp-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cited-mcp
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: MCP server for the Cited GEO platform
|
|
5
|
+
Author-email: Cited <support@youcited.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: cited-core>=0.3.2
|
|
9
|
+
Requires-Dist: mcp>=1.26.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cited-mcp"
|
|
7
|
+
version = "0.3.2"
|
|
8
|
+
description = "MCP server for the Cited GEO platform"
|
|
9
|
+
license = "LicenseRef-Proprietary"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [{ name = "Cited", email = "support@youcited.com" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"cited-core>=0.3.2",
|
|
14
|
+
"mcp>=1.26.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
cited-mcp = "cited_mcp:run_server"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.0",
|
|
23
|
+
"respx>=0.21",
|
|
24
|
+
"ruff>=0.4",
|
|
25
|
+
"mypy>=1.10",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/cited_mcp"]
|
|
@@ -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_core.api.client import CitedClient
|
|
10
|
+
from cited_core.auth.store import TokenStore
|
|
11
|
+
from cited_core.config.manager import ConfigManager
|
|
12
|
+
from cited_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 = os.environ.get("CITED_TOKEN") or 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_mcp.tools.auth # noqa: I001, E402, F401
|
|
45
|
+
import cited_mcp.tools.audit # noqa: E402, F401
|
|
46
|
+
import cited_mcp.tools.business # noqa: E402, F401
|
|
47
|
+
import cited_mcp.tools.job # noqa: E402, F401
|
|
48
|
+
import cited_mcp.tools.recommend # noqa: E402, F401
|
|
49
|
+
import cited_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_core.errors import CitedAPIError
|
|
8
|
+
from cited_mcp.context import CitedContext
|
|
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. Use the 'login' tool or set CITED_TOKEN env var.",
|
|
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
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context
|
|
6
|
+
|
|
7
|
+
from cited_core.api import endpoints
|
|
8
|
+
from cited_core.errors import CitedAPIError
|
|
9
|
+
from cited_mcp.context import CitedContext
|
|
10
|
+
from cited_mcp.server import mcp
|
|
11
|
+
from cited_mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
async def list_audit_templates(ctx: Context[Any, CitedContext, Any]) -> Any:
|
|
16
|
+
"""List all audit templates (named audits) for the user."""
|
|
17
|
+
cited_ctx = _get_ctx(ctx)
|
|
18
|
+
if err := _auth_check(cited_ctx):
|
|
19
|
+
return err
|
|
20
|
+
try:
|
|
21
|
+
return cited_ctx.client.get(endpoints.NAMED_AUDITS)
|
|
22
|
+
except CitedAPIError as e:
|
|
23
|
+
return _api_error_response(e)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
async def get_audit_template(ctx: Context[Any, CitedContext, Any], named_audit_id: str) -> Any:
|
|
28
|
+
"""Get a specific audit template by ID."""
|
|
29
|
+
cited_ctx = _get_ctx(ctx)
|
|
30
|
+
if err := _auth_check(cited_ctx):
|
|
31
|
+
return err
|
|
32
|
+
try:
|
|
33
|
+
return cited_ctx.client.get(
|
|
34
|
+
endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
|
|
35
|
+
)
|
|
36
|
+
except CitedAPIError as e:
|
|
37
|
+
return _api_error_response(e)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
async def create_audit_template(
|
|
42
|
+
ctx: Context[Any, CitedContext, Any],
|
|
43
|
+
name: str,
|
|
44
|
+
business_id: str,
|
|
45
|
+
description: str | None = None,
|
|
46
|
+
questions: list[str] | None = None,
|
|
47
|
+
) -> Any:
|
|
48
|
+
"""Create a new audit template.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
ctx: MCP context
|
|
52
|
+
name: Template name
|
|
53
|
+
business_id: Business ID to associate with
|
|
54
|
+
description: Optional template description
|
|
55
|
+
questions: Optional list of audit questions
|
|
56
|
+
"""
|
|
57
|
+
cited_ctx = _get_ctx(ctx)
|
|
58
|
+
if err := _auth_check(cited_ctx):
|
|
59
|
+
return err
|
|
60
|
+
payload: dict[str, Any] = {"name": name, "business_id": business_id}
|
|
61
|
+
if description is not None:
|
|
62
|
+
payload["description"] = description
|
|
63
|
+
if questions is not None:
|
|
64
|
+
payload["questions"] = questions
|
|
65
|
+
try:
|
|
66
|
+
return cited_ctx.client.post(endpoints.NAMED_AUDITS, json=payload)
|
|
67
|
+
except CitedAPIError as e:
|
|
68
|
+
return _api_error_response(e)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@mcp.tool()
|
|
72
|
+
async def update_audit_template(
|
|
73
|
+
ctx: Context[Any, CitedContext, Any],
|
|
74
|
+
named_audit_id: str,
|
|
75
|
+
name: str | None = None,
|
|
76
|
+
description: str | None = None,
|
|
77
|
+
questions: list[str] | None = None,
|
|
78
|
+
) -> Any:
|
|
79
|
+
"""Update an audit template. Questions replace ALL existing questions if provided.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
ctx: MCP context
|
|
83
|
+
named_audit_id: Template ID to update
|
|
84
|
+
name: New template name
|
|
85
|
+
description: New description
|
|
86
|
+
questions: New list of questions (replaces all existing questions). Omit to keep existing.
|
|
87
|
+
"""
|
|
88
|
+
cited_ctx = _get_ctx(ctx)
|
|
89
|
+
if err := _auth_check(cited_ctx):
|
|
90
|
+
return err
|
|
91
|
+
payload: dict[str, Any] = {}
|
|
92
|
+
if name is not None:
|
|
93
|
+
payload["name"] = name
|
|
94
|
+
if description is not None:
|
|
95
|
+
payload["description"] = description
|
|
96
|
+
if questions is not None:
|
|
97
|
+
payload["questions"] = questions
|
|
98
|
+
try:
|
|
99
|
+
return cited_ctx.client.put(
|
|
100
|
+
endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id), json=payload
|
|
101
|
+
)
|
|
102
|
+
except CitedAPIError as e:
|
|
103
|
+
return _api_error_response(e)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@mcp.tool()
|
|
107
|
+
async def delete_audit_template(
|
|
108
|
+
ctx: Context[Any, CitedContext, Any], named_audit_id: str
|
|
109
|
+
) -> Any:
|
|
110
|
+
"""Delete an audit template.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
ctx: MCP context
|
|
114
|
+
named_audit_id: Template ID to delete
|
|
115
|
+
"""
|
|
116
|
+
cited_ctx = _get_ctx(ctx)
|
|
117
|
+
if err := _auth_check(cited_ctx):
|
|
118
|
+
return err
|
|
119
|
+
try:
|
|
120
|
+
cited_ctx.client.delete(
|
|
121
|
+
endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
|
|
122
|
+
)
|
|
123
|
+
return {"success": True, "message": f"Template {named_audit_id} deleted"}
|
|
124
|
+
except CitedAPIError as e:
|
|
125
|
+
return _api_error_response(e)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@mcp.tool()
|
|
129
|
+
async def start_audit(
|
|
130
|
+
ctx: Context[Any, CitedContext, Any],
|
|
131
|
+
named_audit_id: str,
|
|
132
|
+
business_id: str | None = None,
|
|
133
|
+
) -> Any:
|
|
134
|
+
"""Start an audit using a template. Returns a job_id to poll for status.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
ctx: MCP context
|
|
138
|
+
named_audit_id: The audit template ID to run
|
|
139
|
+
business_id: Optional business ID override
|
|
140
|
+
"""
|
|
141
|
+
cited_ctx = _get_ctx(ctx)
|
|
142
|
+
if err := _auth_check(cited_ctx):
|
|
143
|
+
return err
|
|
144
|
+
payload: dict[str, Any] = {"named_audit_id": named_audit_id}
|
|
145
|
+
if business_id is not None:
|
|
146
|
+
payload["business_id"] = business_id
|
|
147
|
+
try:
|
|
148
|
+
return cited_ctx.client.post(endpoints.AUDIT_START, json=payload)
|
|
149
|
+
except CitedAPIError as e:
|
|
150
|
+
return _api_error_response(e)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@mcp.tool()
|
|
154
|
+
async def get_audit_status(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
155
|
+
"""Check the status of a running audit job."""
|
|
156
|
+
cited_ctx = _get_ctx(ctx)
|
|
157
|
+
if err := _auth_check(cited_ctx):
|
|
158
|
+
return err
|
|
159
|
+
try:
|
|
160
|
+
return cited_ctx.client.get(endpoints.AUDIT_STATUS.format(job_id=job_id))
|
|
161
|
+
except CitedAPIError as e:
|
|
162
|
+
return _api_error_response(e)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@mcp.tool()
|
|
166
|
+
async def get_audit_result(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
167
|
+
"""Get the results of a completed audit job."""
|
|
168
|
+
cited_ctx = _get_ctx(ctx)
|
|
169
|
+
if err := _auth_check(cited_ctx):
|
|
170
|
+
return err
|
|
171
|
+
try:
|
|
172
|
+
return cited_ctx.client.get(endpoints.AUDIT_RESULT.format(job_id=job_id))
|
|
173
|
+
except CitedAPIError as e:
|
|
174
|
+
return _api_error_response(e)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@mcp.tool()
|
|
178
|
+
async def list_audits(ctx: Context[Any, CitedContext, Any]) -> Any:
|
|
179
|
+
"""List audit history."""
|
|
180
|
+
cited_ctx = _get_ctx(ctx)
|
|
181
|
+
if err := _auth_check(cited_ctx):
|
|
182
|
+
return err
|
|
183
|
+
try:
|
|
184
|
+
return cited_ctx.client.get(endpoints.AUDIT_HISTORY)
|
|
185
|
+
except CitedAPIError as e:
|
|
186
|
+
return _api_error_response(e)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import webbrowser
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import anyio
|
|
7
|
+
from mcp.server.fastmcp import Context
|
|
8
|
+
|
|
9
|
+
from cited_core.api import endpoints
|
|
10
|
+
from cited_core.auth.oauth_server import OAuthCallbackServer
|
|
11
|
+
from cited_core.auth.store import TokenStore
|
|
12
|
+
from cited_core.config.constants import FRONTEND_URLS
|
|
13
|
+
from cited_core.errors import CitedAPIError
|
|
14
|
+
from cited_mcp.context import CitedContext
|
|
15
|
+
from cited_mcp.server import mcp
|
|
16
|
+
from cited_mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
async def check_auth_status(ctx: Context[Any, CitedContext, Any]) -> Any:
|
|
21
|
+
"""Check if the user is authenticated and return their account info."""
|
|
22
|
+
cited_ctx = _get_ctx(ctx)
|
|
23
|
+
if err := _auth_check(cited_ctx):
|
|
24
|
+
return err
|
|
25
|
+
try:
|
|
26
|
+
return cited_ctx.client.get(endpoints.ME)
|
|
27
|
+
except CitedAPIError as e:
|
|
28
|
+
return _api_error_response(e)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@mcp.tool()
|
|
32
|
+
async def login(ctx: Context[Any, CitedContext, Any], env: str | None = None) -> Any:
|
|
33
|
+
"""Log in to Cited by opening a browser window for OAuth authentication.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
ctx: MCP context
|
|
37
|
+
env: Optional environment override (dev, prod, local)
|
|
38
|
+
"""
|
|
39
|
+
cited_ctx = _get_ctx(ctx)
|
|
40
|
+
target_env = env or cited_ctx.env
|
|
41
|
+
|
|
42
|
+
callback_server = OAuthCallbackServer(timeout=120.0)
|
|
43
|
+
callback_server.start()
|
|
44
|
+
|
|
45
|
+
frontend = FRONTEND_URLS.get(target_env, FRONTEND_URLS["prod"])
|
|
46
|
+
login_url = f"{frontend}/login?callback={callback_server.redirect_uri}"
|
|
47
|
+
|
|
48
|
+
webbrowser.open(login_url)
|
|
49
|
+
|
|
50
|
+
token = await anyio.to_thread.run_sync(callback_server.wait_for_token)
|
|
51
|
+
callback_server.shutdown()
|
|
52
|
+
|
|
53
|
+
if not token:
|
|
54
|
+
return {"error": True, "message": "Login timed out. Please try again."}
|
|
55
|
+
|
|
56
|
+
TokenStore().save_token(target_env, token)
|
|
57
|
+
cited_ctx.client.token = token
|
|
58
|
+
|
|
59
|
+
return {"success": True, "message": f"Logged in successfully (env: {target_env})"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context
|
|
6
|
+
|
|
7
|
+
from cited_core.api import endpoints
|
|
8
|
+
from cited_core.errors import CitedAPIError
|
|
9
|
+
from cited_mcp.context import CitedContext
|
|
10
|
+
from cited_mcp.server import mcp
|
|
11
|
+
from cited_mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
async def list_businesses(ctx: Context[Any, CitedContext, Any]) -> Any:
|
|
16
|
+
"""List all businesses for the authenticated user."""
|
|
17
|
+
cited_ctx = _get_ctx(ctx)
|
|
18
|
+
if err := _auth_check(cited_ctx):
|
|
19
|
+
return err
|
|
20
|
+
try:
|
|
21
|
+
return cited_ctx.client.get(endpoints.BUSINESSES)
|
|
22
|
+
except CitedAPIError as e:
|
|
23
|
+
return _api_error_response(e)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
async def get_business(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
|
|
28
|
+
"""Get details for a specific business by ID."""
|
|
29
|
+
cited_ctx = _get_ctx(ctx)
|
|
30
|
+
if err := _auth_check(cited_ctx):
|
|
31
|
+
return err
|
|
32
|
+
try:
|
|
33
|
+
return cited_ctx.client.get(endpoints.BUSINESS.format(business_id=business_id))
|
|
34
|
+
except CitedAPIError as e:
|
|
35
|
+
return _api_error_response(e)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
async def create_business(
|
|
40
|
+
ctx: Context[Any, CitedContext, Any],
|
|
41
|
+
name: str,
|
|
42
|
+
website: str,
|
|
43
|
+
description: str,
|
|
44
|
+
industry: str = "technology",
|
|
45
|
+
) -> Any:
|
|
46
|
+
"""Create a new business.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
ctx: MCP context
|
|
50
|
+
name: Business name
|
|
51
|
+
website: Business website (must be a publicly DNS-resolvable domain)
|
|
52
|
+
description: Business description (minimum ~50 characters)
|
|
53
|
+
industry: One of: automotive, beauty, consulting, education, entertainment,
|
|
54
|
+
finance, fitness, government, healthcare, home_services, hospitality,
|
|
55
|
+
legal, manufacturing, non_profit, real_estate, restaurant, retail,
|
|
56
|
+
technology, other
|
|
57
|
+
"""
|
|
58
|
+
cited_ctx = _get_ctx(ctx)
|
|
59
|
+
if err := _auth_check(cited_ctx):
|
|
60
|
+
return err
|
|
61
|
+
try:
|
|
62
|
+
return cited_ctx.client.post(
|
|
63
|
+
endpoints.BUSINESSES,
|
|
64
|
+
json={
|
|
65
|
+
"name": name,
|
|
66
|
+
"website": website,
|
|
67
|
+
"description": description,
|
|
68
|
+
"industry": industry,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
except CitedAPIError as e:
|
|
72
|
+
return _api_error_response(e)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@mcp.tool()
|
|
76
|
+
async def update_business(
|
|
77
|
+
ctx: Context[Any, CitedContext, Any],
|
|
78
|
+
business_id: str,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
website: str | None = None,
|
|
81
|
+
description: str | None = None,
|
|
82
|
+
industry: str | None = None,
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Update an existing business. Only provided fields are changed.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ctx: MCP context
|
|
88
|
+
business_id: Business ID to update
|
|
89
|
+
name: New business name
|
|
90
|
+
website: New website URL
|
|
91
|
+
description: New description
|
|
92
|
+
industry: New industry value
|
|
93
|
+
"""
|
|
94
|
+
cited_ctx = _get_ctx(ctx)
|
|
95
|
+
if err := _auth_check(cited_ctx):
|
|
96
|
+
return err
|
|
97
|
+
payload: dict[str, Any] = {}
|
|
98
|
+
if name is not None:
|
|
99
|
+
payload["name"] = name
|
|
100
|
+
if website is not None:
|
|
101
|
+
payload["website"] = website
|
|
102
|
+
if description is not None:
|
|
103
|
+
payload["description"] = description
|
|
104
|
+
if industry is not None:
|
|
105
|
+
payload["industry"] = industry
|
|
106
|
+
try:
|
|
107
|
+
return cited_ctx.client.put(
|
|
108
|
+
endpoints.BUSINESS.format(business_id=business_id), json=payload
|
|
109
|
+
)
|
|
110
|
+
except CitedAPIError as e:
|
|
111
|
+
return _api_error_response(e)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@mcp.tool()
|
|
115
|
+
async def delete_business(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
|
|
116
|
+
"""Delete a business and all its associated data.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
ctx: MCP context
|
|
120
|
+
business_id: Business ID to delete
|
|
121
|
+
"""
|
|
122
|
+
cited_ctx = _get_ctx(ctx)
|
|
123
|
+
if err := _auth_check(cited_ctx):
|
|
124
|
+
return err
|
|
125
|
+
try:
|
|
126
|
+
cited_ctx.client.delete(endpoints.BUSINESS.format(business_id=business_id))
|
|
127
|
+
return {"success": True, "message": f"Business {business_id} deleted"}
|
|
128
|
+
except CitedAPIError as e:
|
|
129
|
+
return _api_error_response(e)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
async def crawl_business(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
|
|
134
|
+
"""Start a crawl job for a business. Returns a job_id you can poll with get_job_status."""
|
|
135
|
+
cited_ctx = _get_ctx(ctx)
|
|
136
|
+
if err := _auth_check(cited_ctx):
|
|
137
|
+
return err
|
|
138
|
+
try:
|
|
139
|
+
return cited_ctx.client.post(endpoints.CRAWL_START.format(business_id=business_id))
|
|
140
|
+
except CitedAPIError as e:
|
|
141
|
+
return _api_error_response(e)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
async def get_health_scores(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
|
|
146
|
+
"""Get GEO health scores for a business."""
|
|
147
|
+
cited_ctx = _get_ctx(ctx)
|
|
148
|
+
if err := _auth_check(cited_ctx):
|
|
149
|
+
return err
|
|
150
|
+
try:
|
|
151
|
+
return cited_ctx.client.get(endpoints.HEALTH_SCORES.format(business_id=business_id))
|
|
152
|
+
except CitedAPIError as e:
|
|
153
|
+
return _api_error_response(e)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context
|
|
6
|
+
|
|
7
|
+
from cited_core.api import endpoints
|
|
8
|
+
from cited_core.errors import CitedAPIError
|
|
9
|
+
from cited_mcp.context import CitedContext
|
|
10
|
+
from cited_mcp.server import mcp
|
|
11
|
+
from cited_mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
|
|
12
|
+
|
|
13
|
+
_STATUS_ENDPOINTS = {
|
|
14
|
+
"audit": endpoints.AUDIT_STATUS,
|
|
15
|
+
"recommendation": endpoints.RECOMMEND_STATUS,
|
|
16
|
+
"solution": endpoints.SOLUTION_STATUS,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
async def get_job_status(
|
|
22
|
+
ctx: Context[Any, CitedContext, Any],
|
|
23
|
+
job_id: str,
|
|
24
|
+
job_type: str | None = None,
|
|
25
|
+
) -> Any:
|
|
26
|
+
"""Get the status of any job by ID.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
ctx: MCP context
|
|
30
|
+
job_id: The job ID to check
|
|
31
|
+
job_type: Optional job type hint: audit, recommendation, or solution.
|
|
32
|
+
If not provided, will probe each type until one responds.
|
|
33
|
+
"""
|
|
34
|
+
cited_ctx = _get_ctx(ctx)
|
|
35
|
+
if err := _auth_check(cited_ctx):
|
|
36
|
+
return err
|
|
37
|
+
|
|
38
|
+
if job_type and job_type in _STATUS_ENDPOINTS:
|
|
39
|
+
try:
|
|
40
|
+
result = cited_ctx.client.get(
|
|
41
|
+
_STATUS_ENDPOINTS[job_type].format(job_id=job_id)
|
|
42
|
+
)
|
|
43
|
+
result["job_type"] = job_type
|
|
44
|
+
return result
|
|
45
|
+
except CitedAPIError as e:
|
|
46
|
+
return _api_error_response(e)
|
|
47
|
+
|
|
48
|
+
# Probe each type
|
|
49
|
+
for jtype, endpoint_tpl in _STATUS_ENDPOINTS.items():
|
|
50
|
+
try:
|
|
51
|
+
result = cited_ctx.client.get(endpoint_tpl.format(job_id=job_id))
|
|
52
|
+
result["job_type"] = jtype
|
|
53
|
+
return result
|
|
54
|
+
except CitedAPIError:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
return {"error": True, "message": f"No job found with ID: {job_id}"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context
|
|
6
|
+
|
|
7
|
+
from cited_core.api import endpoints
|
|
8
|
+
from cited_core.errors import CitedAPIError
|
|
9
|
+
from cited_mcp.context import CitedContext
|
|
10
|
+
from cited_mcp.server import mcp
|
|
11
|
+
from cited_mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
async def start_recommendation(ctx: Context[Any, CitedContext, Any], audit_job_id: str) -> Any:
|
|
16
|
+
"""Start a recommendation job based on a completed audit.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
ctx: MCP context
|
|
20
|
+
audit_job_id: The completed audit job ID to generate recommendations for
|
|
21
|
+
"""
|
|
22
|
+
cited_ctx = _get_ctx(ctx)
|
|
23
|
+
if err := _auth_check(cited_ctx):
|
|
24
|
+
return err
|
|
25
|
+
try:
|
|
26
|
+
return cited_ctx.client.post(
|
|
27
|
+
endpoints.RECOMMEND_START,
|
|
28
|
+
json={"audit_job_id": audit_job_id},
|
|
29
|
+
)
|
|
30
|
+
except CitedAPIError as e:
|
|
31
|
+
return _api_error_response(e)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
async def get_recommendation_status(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
36
|
+
"""Check the status of a recommendation job."""
|
|
37
|
+
cited_ctx = _get_ctx(ctx)
|
|
38
|
+
if err := _auth_check(cited_ctx):
|
|
39
|
+
return err
|
|
40
|
+
try:
|
|
41
|
+
return cited_ctx.client.get(endpoints.RECOMMEND_STATUS.format(job_id=job_id))
|
|
42
|
+
except CitedAPIError as e:
|
|
43
|
+
return _api_error_response(e)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool()
|
|
47
|
+
async def get_recommendation_result(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
48
|
+
"""Get the full results of a completed recommendation job."""
|
|
49
|
+
cited_ctx = _get_ctx(ctx)
|
|
50
|
+
if err := _auth_check(cited_ctx):
|
|
51
|
+
return err
|
|
52
|
+
try:
|
|
53
|
+
return cited_ctx.client.get(endpoints.RECOMMEND_RESULT.format(job_id=job_id))
|
|
54
|
+
except CitedAPIError as e:
|
|
55
|
+
return _api_error_response(e)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
async def get_recommendation_insights(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
60
|
+
"""Get actionable insights from a recommendation, with source_type and source_id for solutions.
|
|
61
|
+
|
|
62
|
+
Returns question_insights, head_to_head_comparisons, strengthening_tips, and
|
|
63
|
+
priority_actions, each annotated with the source_type and source_id needed to
|
|
64
|
+
start a solution via start_solution.
|
|
65
|
+
"""
|
|
66
|
+
cited_ctx = _get_ctx(ctx)
|
|
67
|
+
if err := _auth_check(cited_ctx):
|
|
68
|
+
return err
|
|
69
|
+
try:
|
|
70
|
+
result = cited_ctx.client.get(endpoints.RECOMMEND_RESULT.format(job_id=job_id))
|
|
71
|
+
except CitedAPIError as e:
|
|
72
|
+
return _api_error_response(e)
|
|
73
|
+
|
|
74
|
+
insights: dict[str, Any] = {}
|
|
75
|
+
|
|
76
|
+
for item in result.get("question_insights", []):
|
|
77
|
+
item["source_type"] = "question_insight"
|
|
78
|
+
item["source_id"] = item.get("question_id", "")
|
|
79
|
+
insights["question_insights"] = result.get("question_insights", [])
|
|
80
|
+
|
|
81
|
+
for item in result.get("head_to_head_comparisons", []):
|
|
82
|
+
item["source_type"] = "head_to_head"
|
|
83
|
+
item["source_id"] = item.get("competitor_domain", "")
|
|
84
|
+
insights["head_to_head_comparisons"] = result.get("head_to_head_comparisons", [])
|
|
85
|
+
|
|
86
|
+
for item in result.get("strengthening_tips", []):
|
|
87
|
+
item["source_type"] = "strengthening_tip"
|
|
88
|
+
item["source_id"] = item.get("category", "")
|
|
89
|
+
insights["strengthening_tips"] = result.get("strengthening_tips", [])
|
|
90
|
+
|
|
91
|
+
for item in result.get("priority_actions", []):
|
|
92
|
+
item["source_type"] = "priority_action"
|
|
93
|
+
item["source_id"] = item.get("id", item.get("category", ""))
|
|
94
|
+
insights["priority_actions"] = result.get("priority_actions", [])
|
|
95
|
+
|
|
96
|
+
return insights
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@mcp.tool()
|
|
100
|
+
async def list_recommendations(
|
|
101
|
+
ctx: Context[Any, CitedContext, Any], audit_job_id: str
|
|
102
|
+
) -> Any:
|
|
103
|
+
"""List recommendation history for a specific audit.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
ctx: MCP context
|
|
107
|
+
audit_job_id: The audit job ID to list recommendations for
|
|
108
|
+
"""
|
|
109
|
+
cited_ctx = _get_ctx(ctx)
|
|
110
|
+
if err := _auth_check(cited_ctx):
|
|
111
|
+
return err
|
|
112
|
+
try:
|
|
113
|
+
return cited_ctx.client.get(
|
|
114
|
+
endpoints.RECOMMEND_HISTORY.format(audit_job_id=audit_job_id)
|
|
115
|
+
)
|
|
116
|
+
except CitedAPIError as e:
|
|
117
|
+
return _api_error_response(e)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context
|
|
6
|
+
|
|
7
|
+
from cited_core.api import endpoints
|
|
8
|
+
from cited_core.errors import CitedAPIError
|
|
9
|
+
from cited_mcp.context import CitedContext
|
|
10
|
+
from cited_mcp.server import mcp
|
|
11
|
+
from cited_mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
async def start_solution(
|
|
16
|
+
ctx: Context[Any, CitedContext, Any],
|
|
17
|
+
recommendation_job_id: str,
|
|
18
|
+
source_type: str,
|
|
19
|
+
source_id: str,
|
|
20
|
+
) -> Any:
|
|
21
|
+
"""Start generating a solution for a specific recommendation insight.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
ctx: MCP context
|
|
25
|
+
recommendation_job_id: The recommendation job ID
|
|
26
|
+
source_type: One of: question_insight, head_to_head, strengthening_tip, priority_action
|
|
27
|
+
source_id: The source identifier (question_id, competitor_domain, category, etc.)
|
|
28
|
+
"""
|
|
29
|
+
cited_ctx = _get_ctx(ctx)
|
|
30
|
+
if err := _auth_check(cited_ctx):
|
|
31
|
+
return err
|
|
32
|
+
try:
|
|
33
|
+
return cited_ctx.client.post(
|
|
34
|
+
endpoints.SOLUTION_REQUEST,
|
|
35
|
+
json={
|
|
36
|
+
"recommendation_job_id": recommendation_job_id,
|
|
37
|
+
"source_type": source_type,
|
|
38
|
+
"source_id": source_id,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
except CitedAPIError as e:
|
|
42
|
+
return _api_error_response(e)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
async def get_solution_status(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
47
|
+
"""Check the status of a solution generation job."""
|
|
48
|
+
cited_ctx = _get_ctx(ctx)
|
|
49
|
+
if err := _auth_check(cited_ctx):
|
|
50
|
+
return err
|
|
51
|
+
try:
|
|
52
|
+
return cited_ctx.client.get(endpoints.SOLUTION_STATUS.format(job_id=job_id))
|
|
53
|
+
except CitedAPIError as e:
|
|
54
|
+
return _api_error_response(e)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@mcp.tool()
|
|
58
|
+
async def get_solution_result(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
|
|
59
|
+
"""Get the results of a completed solution generation job."""
|
|
60
|
+
cited_ctx = _get_ctx(ctx)
|
|
61
|
+
if err := _auth_check(cited_ctx):
|
|
62
|
+
return err
|
|
63
|
+
try:
|
|
64
|
+
return cited_ctx.client.get(endpoints.SOLUTION_RESULT.format(job_id=job_id))
|
|
65
|
+
except CitedAPIError as e:
|
|
66
|
+
return _api_error_response(e)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
async def list_solutions(ctx: Context[Any, CitedContext, Any]) -> Any:
|
|
71
|
+
"""List solution history."""
|
|
72
|
+
cited_ctx = _get_ctx(ctx)
|
|
73
|
+
if err := _auth_check(cited_ctx):
|
|
74
|
+
return err
|
|
75
|
+
try:
|
|
76
|
+
return cited_ctx.client.get(endpoints.SOLUTION_HISTORY)
|
|
77
|
+
except CitedAPIError as e:
|
|
78
|
+
return _api_error_response(e)
|