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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepsieve-mcp
3
+ Version: 0.1.0
4
+ Summary: DeepSieve MCP server — give your agent a cited, self-updating research dataset
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: mcp>=1.2.0
@@ -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()