deepsieve-mcp 0.1.0__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.
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# DeepSieve MCP server
|
|
2
|
+
|
|
3
|
+
Gives any MCP client (Claude Code, Cursor, Claude Desktop, …) nine
|
|
4
|
+
outcome-shaped tools over the DeepSieve `/v1` API: verify the account,
|
|
5
|
+
discover the Blueprint schema, start/poll/cancel research runs, read the
|
|
6
|
+
cited dataset incrementally, export it, and inspect webhooks.
|
|
7
|
+
|
|
8
|
+
It is a plain API client — it holds **no** privileged access, only the
|
|
9
|
+
scoped key you give it (mint one with the **Agent** preset at
|
|
10
|
+
`/settings/api-keys`; it can run research and read results but can't touch
|
|
11
|
+
your schema or billing).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Claude Code
|
|
17
|
+
claude mcp add deepsieve \
|
|
18
|
+
--env DEEPSIEVE_API_KEY=ds_live_... \
|
|
19
|
+
--env DEEPSIEVE_API_URL=https://your-deployment.example.com \
|
|
20
|
+
-- uvx --from /path/to/deep-sieve/mcp-server deepsieve-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Manual `mcpServers` JSON (Cursor, Claude Desktop):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"deepsieve": {
|
|
29
|
+
"command": "uvx",
|
|
30
|
+
"args": ["--from", "/path/to/deep-sieve/mcp-server", "deepsieve-mcp"],
|
|
31
|
+
"env": {
|
|
32
|
+
"DEEPSIEVE_API_KEY": "ds_live_...",
|
|
33
|
+
"DEEPSIEVE_API_URL": "https://your-deployment.example.com"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`DEEPSIEVE_API_URL` defaults to `http://localhost:8200` (local dev stack).
|
|
41
|
+
|
|
42
|
+
## Tools
|
|
43
|
+
|
|
44
|
+
`get_account` · `get_blueprint` · `start_research` · `get_run_status` ·
|
|
45
|
+
`list_runs` · `cancel_run` · `query_entities` · `export_dataset` ·
|
|
46
|
+
`list_webhooks`
|
|
47
|
+
|
|
48
|
+
Full API docs: `/developers` on your deployment (also `/llms.txt`).
|
|
49
|
+
PyPI/npm publishing and a hosted Streamable-HTTP OAuth server are tracked
|
|
50
|
+
follow-ups (issue #116 Phase-2).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "deepsieve-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "DeepSieve MCP server — give your agent a cited, self-updating research dataset"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = ["mcp>=1.2.0", "httpx>=0.27"]
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
deepsieve-mcp = "deepsieve_mcp.server:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/deepsieve_mcp"]
|
|
File without changes
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""DeepSieve MCP server (#116 item 14) — stdio transport.
|
|
2
|
+
|
|
3
|
+
Nine outcome-shaped tools over the public ``/v1`` REST API. Deliberately NOT a
|
|
4
|
+
1:1 endpoint wrap: flat arguments, summaries + fetch handles, read-only by
|
|
5
|
+
default. Every call rides the same auth + tenancy + scope guards as any other
|
|
6
|
+
API client — the server holds no privileged access, only the key you give it:
|
|
7
|
+
|
|
8
|
+
export DEEPSIEVE_API_KEY=ds_live_... (required)
|
|
9
|
+
export DEEPSIEVE_API_URL=https://<your-deployment> (default local dev)
|
|
10
|
+
|
|
11
|
+
Install for Claude Code:
|
|
12
|
+
claude mcp add deepsieve --env DEEPSIEVE_API_KEY=ds_live_... \
|
|
13
|
+
-- uvx --from /path/to/deep-sieve/mcp-server deepsieve-mcp
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import uuid as _uuid
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
from mcp.server.fastmcp import FastMCP
|
|
25
|
+
|
|
26
|
+
mcp = FastMCP("deepsieve")
|
|
27
|
+
|
|
28
|
+
DEFAULT_URL = "http://localhost:8200"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _client() -> httpx.Client:
|
|
32
|
+
key = os.environ.get("DEEPSIEVE_API_KEY")
|
|
33
|
+
headers = {"Authorization": f"Bearer {key}"} if key else {}
|
|
34
|
+
return httpx.Client(
|
|
35
|
+
base_url=os.environ.get("DEEPSIEVE_API_URL", DEFAULT_URL),
|
|
36
|
+
headers=headers,
|
|
37
|
+
timeout=30,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _call(method: str, path: str, **kwargs) -> Any:
|
|
42
|
+
"""One API call → parsed JSON, with the typed error surfaced verbatim so
|
|
43
|
+
the agent can read `code`/`retriable` and self-correct."""
|
|
44
|
+
with _client() as c:
|
|
45
|
+
r = c.request(method, path, **kwargs)
|
|
46
|
+
try:
|
|
47
|
+
body = r.json()
|
|
48
|
+
except ValueError:
|
|
49
|
+
r.raise_for_status()
|
|
50
|
+
return {"raw": r.text}
|
|
51
|
+
if not r.is_success:
|
|
52
|
+
err = body.get("error") or {}
|
|
53
|
+
return {
|
|
54
|
+
"error": True,
|
|
55
|
+
"code": err.get("code", str(r.status_code)),
|
|
56
|
+
"message": err.get("message", r.text[:300]),
|
|
57
|
+
"retriable": err.get("retriable", False),
|
|
58
|
+
"request_id": err.get("request_id"),
|
|
59
|
+
}
|
|
60
|
+
return body
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@mcp.tool()
|
|
64
|
+
def get_account() -> str:
|
|
65
|
+
"""Verify the API key works: who am I, which workspace, which scopes."""
|
|
66
|
+
return json.dumps(_call("GET", "/v1/me"), default=str)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
def get_blueprint() -> str:
|
|
71
|
+
"""The active Blueprint's entity catalog — the customer-defined schema
|
|
72
|
+
(entities + columns). Call this before querying data; never guess columns."""
|
|
73
|
+
return json.dumps(_call("GET", "/v1/data"), default=str)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@mcp.tool()
|
|
77
|
+
def start_research(
|
|
78
|
+
query: str = "",
|
|
79
|
+
seeds: list[str] | None = None,
|
|
80
|
+
guidance: str = "",
|
|
81
|
+
depth: str = "standard",
|
|
82
|
+
idempotency_key: str = "",
|
|
83
|
+
dry_run: bool = False,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Start an async research run from a natural-language query OR a list of
|
|
86
|
+
seed URLs/names (exactly one). Returns 202 with a run id — the run takes
|
|
87
|
+
minutes; poll get_run_status, don't wait inline. Runs cost real money:
|
|
88
|
+
pass an idempotency_key so your retries never double-bill. dry_run=True is
|
|
89
|
+
FREE: behaves like a real run (202, poll get_run_status, completes in ~15
|
|
90
|
+
simulated seconds) and returns sample cited rows in the real schema — use
|
|
91
|
+
it to build/test the whole poll loop before spending credits."""
|
|
92
|
+
payload: dict[str, Any] = {"depth": depth, "dry_run": dry_run}
|
|
93
|
+
if query:
|
|
94
|
+
payload["query"] = query
|
|
95
|
+
if seeds:
|
|
96
|
+
payload["seeds"] = seeds
|
|
97
|
+
if guidance:
|
|
98
|
+
payload["guidance"] = guidance
|
|
99
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
|
|
100
|
+
return json.dumps(_call("POST", "/v1/research/runs", json=payload, headers=headers))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _uuid_or_error(run_id: str) -> str | None:
|
|
104
|
+
"""run_id is interpolated into a URL path — reject non-UUIDs so a value
|
|
105
|
+
like '../keys' can't reach a different endpoint."""
|
|
106
|
+
try:
|
|
107
|
+
return str(_uuid.UUID(run_id))
|
|
108
|
+
except ValueError:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mcp.tool()
|
|
113
|
+
def get_run_status(run_id: str) -> str:
|
|
114
|
+
"""Poll a research run: status, done flag, error, progress, metrics."""
|
|
115
|
+
rid = _uuid_or_error(run_id)
|
|
116
|
+
if rid is None:
|
|
117
|
+
return json.dumps({"error": True, "code": "invalid_request", "message": "run_id must be a UUID"})
|
|
118
|
+
return json.dumps(_call("GET", f"/v1/research/runs/{rid}"), default=str)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@mcp.tool()
|
|
122
|
+
def list_runs(status: str = "", limit: int = 20) -> str:
|
|
123
|
+
"""Recent research runs (newest first). Optional status filter:
|
|
124
|
+
queued|running|cancelling|completed|failed|cancelled."""
|
|
125
|
+
params: dict[str, Any] = {"limit": limit}
|
|
126
|
+
if status:
|
|
127
|
+
params["status"] = status
|
|
128
|
+
return json.dumps(_call("GET", "/v1/research/runs", params=params), default=str)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
def cancel_run(run_id: str) -> str:
|
|
133
|
+
"""Cancel an in-flight research run."""
|
|
134
|
+
rid = _uuid_or_error(run_id)
|
|
135
|
+
if rid is None:
|
|
136
|
+
return json.dumps({"error": True, "code": "invalid_request", "message": "run_id must be a UUID"})
|
|
137
|
+
return json.dumps(_call("POST", f"/v1/research/runs/{rid}/cancel"), default=str)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@mcp.tool()
|
|
141
|
+
def query_entities(
|
|
142
|
+
entity_key: str,
|
|
143
|
+
limit: int = 25,
|
|
144
|
+
cursor: str = "",
|
|
145
|
+
updated_since: str = "",
|
|
146
|
+
fields: str = "",
|
|
147
|
+
receipts: bool = False,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Read the cited dataset for one entity (cross-run canonical rows).
|
|
150
|
+
`updated_since` (ISO timestamp) turns this into an incremental sync;
|
|
151
|
+
`receipts=True` nests per-cell citations (value, confidence, evidence id,
|
|
152
|
+
source URLs, retrieved_at); `fields` is a comma-separated column subset.
|
|
153
|
+
Get entity keys + columns from get_blueprint first."""
|
|
154
|
+
params: dict[str, Any] = {"limit": limit, "receipts": receipts}
|
|
155
|
+
if cursor:
|
|
156
|
+
params["cursor"] = cursor
|
|
157
|
+
if updated_since:
|
|
158
|
+
params["updated_since"] = updated_since
|
|
159
|
+
if fields:
|
|
160
|
+
params["fields"] = fields
|
|
161
|
+
return json.dumps(_call("GET", f"/v1/data/{entity_key}", params=params), default=str)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@mcp.tool()
|
|
165
|
+
def export_dataset(format: str = "ndjson", entity: str = "") -> str:
|
|
166
|
+
"""Export the whole dataset (or one entity) with citations. Formats:
|
|
167
|
+
ndjson (default; first line is schema metadata), json, csv (csv requires
|
|
168
|
+
entity). Large datasets: prefer query_entities pagination."""
|
|
169
|
+
params: dict[str, Any] = {"format": format}
|
|
170
|
+
if entity:
|
|
171
|
+
params["entity"] = entity
|
|
172
|
+
with _client() as c:
|
|
173
|
+
r = c.get("/v1/export", params=params)
|
|
174
|
+
if not r.is_success:
|
|
175
|
+
try:
|
|
176
|
+
err = r.json().get("error") or {}
|
|
177
|
+
except ValueError:
|
|
178
|
+
err = {}
|
|
179
|
+
return json.dumps(
|
|
180
|
+
{
|
|
181
|
+
"error": True,
|
|
182
|
+
"code": err.get("code", str(r.status_code)),
|
|
183
|
+
"message": err.get("message", r.text[:300]),
|
|
184
|
+
"retriable": err.get("retriable", False),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
text = r.text
|
|
188
|
+
if len(text) > 200_000:
|
|
189
|
+
return text[:200_000] + "\n...[truncated — use query_entities to paginate]"
|
|
190
|
+
return text
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@mcp.tool()
|
|
194
|
+
def list_reports() -> str:
|
|
195
|
+
"""Saved custom reports (curated cross-run views): id, name, config."""
|
|
196
|
+
return json.dumps(_call("GET", "/v1/reports"), default=str)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@mcp.tool()
|
|
200
|
+
def get_report_columns() -> str:
|
|
201
|
+
"""The valid column vocabulary per entity for building a report config —
|
|
202
|
+
call before create_report; never guess columns."""
|
|
203
|
+
return json.dumps(_call("GET", "/v1/reports/columns"), default=str)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
def create_report(name: str, config: dict) -> str:
|
|
208
|
+
"""Create a saved report. `config` = {"tabs": [{"id", "name", "entity_key",
|
|
209
|
+
"columns": [...]}]} using columns from get_report_columns."""
|
|
210
|
+
return json.dumps(
|
|
211
|
+
_call("POST", "/v1/reports", json={"name": name, "config": config}), default=str
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@mcp.tool()
|
|
216
|
+
def get_report_data(report_id: str, tab_id: str) -> str:
|
|
217
|
+
"""Fresh cross-run rows for one report tab (with per-cell evidence)."""
|
|
218
|
+
rid = _uuid_or_error(report_id)
|
|
219
|
+
if rid is None:
|
|
220
|
+
return json.dumps({"error": True, "code": "invalid_request", "message": "report_id must be a UUID"})
|
|
221
|
+
return json.dumps(_call("GET", f"/v1/reports/{rid}/tabs/{tab_id}/data"), default=str)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@mcp.tool()
|
|
225
|
+
def create_blueprint(prompt: str, idempotency_key: str = "") -> str:
|
|
226
|
+
"""Start creating a NEW Blueprint from a natural-language description of
|
|
227
|
+
the research domain. Returns a session id — poll get_blueprint_draft until
|
|
228
|
+
status 'ready' (review the draft + follow-up questions), optionally
|
|
229
|
+
adjust_blueprint, then approve_blueprint. Editing an ACTIVE Blueprint is
|
|
230
|
+
human-only; this creates a new one."""
|
|
231
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
|
|
232
|
+
return json.dumps(
|
|
233
|
+
_call("POST", "/v1/blueprints/onboarding", json={"prompt": prompt}, headers=headers),
|
|
234
|
+
default=str,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@mcp.tool()
|
|
239
|
+
def get_blueprint_draft(session_id: str) -> str:
|
|
240
|
+
"""Poll an onboarding session: status, draft Blueprint, follow-up questions."""
|
|
241
|
+
sid = _uuid_or_error(session_id)
|
|
242
|
+
if sid is None:
|
|
243
|
+
return json.dumps({"error": True, "code": "invalid_request", "message": "session_id must be a UUID"})
|
|
244
|
+
return json.dumps(_call("GET", f"/v1/blueprints/onboarding/{sid}"), default=str)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@mcp.tool()
|
|
248
|
+
def adjust_blueprint(session_id: str, message: str) -> str:
|
|
249
|
+
"""Ask the Blueprint-inference agent for a change to the DRAFT (e.g. 'add a
|
|
250
|
+
founded_year column to companies'). 202 — poll get_blueprint_draft until
|
|
251
|
+
status returns to 'ready', then re-review."""
|
|
252
|
+
sid = _uuid_or_error(session_id)
|
|
253
|
+
if sid is None:
|
|
254
|
+
return json.dumps({"error": True, "code": "invalid_request", "message": "session_id must be a UUID"})
|
|
255
|
+
return json.dumps(
|
|
256
|
+
_call("POST", f"/v1/blueprints/onboarding/{sid}/adjust", json={"message": message}),
|
|
257
|
+
default=str,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@mcp.tool()
|
|
262
|
+
def approve_blueprint(session_id: str) -> str:
|
|
263
|
+
"""Approve the draft → real tables are created and the workspace activates.
|
|
264
|
+
When acting for a human, show them the draft first — this instantiates
|
|
265
|
+
schema. Poll get_blueprint_draft until status 'active'."""
|
|
266
|
+
sid = _uuid_or_error(session_id)
|
|
267
|
+
if sid is None:
|
|
268
|
+
return json.dumps({"error": True, "code": "invalid_request", "message": "session_id must be a UUID"})
|
|
269
|
+
return json.dumps(_call("POST", f"/v1/blueprints/onboarding/{sid}/approve"), default=str)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mcp.tool()
|
|
273
|
+
def list_webhooks() -> str:
|
|
274
|
+
"""Registered webhook endpoints for this workspace (run.completed/failed/
|
|
275
|
+
cancelled + dataset.updated events, Standard Webhooks signatures)."""
|
|
276
|
+
return json.dumps(_call("GET", "/v1/webhooks"), default=str)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def main() -> None:
|
|
280
|
+
mcp.run() # stdio transport
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
main()
|