deepquery-sdk 1.0.0__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.
deepquery_sdk/gate.py ADDED
@@ -0,0 +1,83 @@
1
+ """The approval gate — the runtime mechanism behind preview → execute.
2
+
3
+ Every action passes through this gate. Calling an action *requests* it: the gate
4
+ records the previewed effect and mints a single-use, argument-bound approval
5
+ token. Execution requires presenting that token. The human-confirmation decision
6
+ itself lives in the Agent Layer / gateway above the SDK (SDK_GUIDE.md §4.3, §5);
7
+ this gate is what makes "execute can't happen without a preview of these exact
8
+ arguments" enforceable at the connector boundary.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import secrets
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, Field
19
+
20
+
21
+ class ActionStatus(str, Enum):
22
+ PREVIEWED = "previewed" # preview generated, awaiting a decision
23
+ EXECUTED = "executed" # approved and run (terminal)
24
+ REJECTED = "rejected" # declined, never run (terminal)
25
+
26
+
27
+ class PendingAction(BaseModel):
28
+ """One requested-but-not-yet-decided action, keyed by its approval token."""
29
+
30
+ token: str
31
+ action_name: str
32
+ arguments: dict[str, Any]
33
+ preview: str
34
+ status: ActionStatus = ActionStatus.PREVIEWED
35
+ requested_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
36
+
37
+
38
+ class GateError(Exception):
39
+ """Raised when the gate is driven into an invalid state transition."""
40
+
41
+
42
+ class ApprovalGate:
43
+ """In-process registry of pending actions and their lifecycle."""
44
+
45
+ def __init__(self) -> None:
46
+ self._pending: dict[str, PendingAction] = {}
47
+
48
+ def request(self, action_name: str, arguments: dict[str, Any], preview: str) -> PendingAction:
49
+ """Record a previewed action and mint its single-use approval token."""
50
+ token = secrets.token_urlsafe(24)
51
+ pending = PendingAction(
52
+ token=token,
53
+ action_name=action_name,
54
+ arguments=dict(arguments),
55
+ preview=preview,
56
+ )
57
+ self._pending[token] = pending
58
+ return pending
59
+
60
+ def get(self, token: str) -> PendingAction:
61
+ pending = self._pending.get(token)
62
+ if pending is None:
63
+ raise GateError(f"unknown or already-consumed approval token: {token!r}")
64
+ return pending
65
+
66
+ def mark_executed(self, token: str) -> PendingAction:
67
+ """Transition PREVIEWED → EXECUTED. The token is consumed (single use)."""
68
+ pending = self.get(token)
69
+ if pending.status is not ActionStatus.PREVIEWED:
70
+ raise GateError(f"action {pending.action_name!r} is already {pending.status.value}")
71
+ pending.status = ActionStatus.EXECUTED
72
+ return pending
73
+
74
+ def reject(self, token: str) -> PendingAction:
75
+ """Transition PREVIEWED → REJECTED. The token is consumed (single use)."""
76
+ pending = self.get(token)
77
+ if pending.status is not ActionStatus.PREVIEWED:
78
+ raise GateError(f"action {pending.action_name!r} is already {pending.status.value}")
79
+ pending.status = ActionStatus.REJECTED
80
+ return pending
81
+
82
+ def pending_tokens(self) -> list[str]:
83
+ return [t for t, p in self._pending.items() if p.status is ActionStatus.PREVIEWED]
@@ -0,0 +1,5 @@
1
+ """Local dev harness for testing connectors without the full platform."""
2
+
3
+ from .mock_agent import HarnessError, MockAgent
4
+
5
+ __all__ = ["MockAgent", "HarnessError"]
@@ -0,0 +1,119 @@
1
+ """Local dev harness — a mock agent that drives a connector like Deep Query would.
2
+
3
+ The mock agent discovers a connector's capabilities through its emitted MCP
4
+ interface (a real in-memory client session, not direct method calls), lets the
5
+ developer issue reads and inspect the provenance envelope, and drives an action
6
+ through the exact preview → approval → execute sequence the real Agent Layer
7
+ uses — including simulating both an approval and a rejection (SDK_GUIDE.md §10).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from mcp.shared.memory import create_connected_server_and_client_session
15
+
16
+ from ..connector import Connector
17
+ from ..mcp_emit.emitter import EXECUTE_TOOL, REJECT_TOOL, emit_server
18
+
19
+
20
+ class HarnessError(Exception):
21
+ pass
22
+
23
+
24
+ class MockAgent:
25
+ """Drives a connector over an in-memory MCP session."""
26
+
27
+ def __init__(self, connector: Connector) -> None:
28
+ self.connector = connector
29
+ self._cm = None
30
+ self.session = None
31
+
32
+ async def __aenter__(self) -> "MockAgent":
33
+ self._cm = create_connected_server_and_client_session(emit_server(self.connector))
34
+ self.session = await self._cm.__aenter__()
35
+ await self.session.initialize()
36
+ return self
37
+
38
+ async def __aexit__(self, *exc) -> None:
39
+ assert self._cm is not None
40
+ await self._cm.__aexit__(*exc)
41
+
42
+ # -- discovery --------------------------------------------------------
43
+ async def capabilities(self) -> dict[str, list[dict[str, Any]]]:
44
+ """Group the advertised tools by kind, with their dq.* classification."""
45
+ tools = (await self.session.list_tools()).tools
46
+ grouped: dict[str, list[dict[str, Any]]] = {"resources": [], "actions": [], "controls": []}
47
+ for t in tools:
48
+ meta = t.meta or {}
49
+ kind = meta.get("dq.kind", "unknown")
50
+ entry = {"name": t.name, "description": t.description, "dq.mutates": meta.get("dq.mutates")}
51
+ if kind == "resource":
52
+ grouped["resources"].append(entry)
53
+ elif kind == "action":
54
+ grouped["actions"].append(entry)
55
+ else:
56
+ grouped["controls"].append(entry)
57
+ return grouped
58
+
59
+ # -- reads ------------------------------------------------------------
60
+ async def read(self, name: str, **arguments: Any) -> list[dict[str, Any]]:
61
+ """Call a read tool and return its records, validating the provenance
62
+ envelope the way the gateway would (surfacing problems as errors)."""
63
+ result = await self.session.call_tool(name, arguments)
64
+ if result.isError:
65
+ raise HarnessError(self._error_text(result, name))
66
+ payload = result.structuredContent or {}
67
+ records = payload.get("records")
68
+ if records is None:
69
+ raise HarnessError(f"read '{name}' did not return a 'records' list")
70
+ for i, rec in enumerate(records):
71
+ prov = rec.get("provenance")
72
+ if not prov:
73
+ raise HarnessError(f"record {i} from '{name}' is missing its provenance envelope (§6)")
74
+ for required in ("connector_name", "source_object_id", "retrieved_at", "title_or_label"):
75
+ if not prov.get(required):
76
+ raise HarnessError(
77
+ f"record {i} from '{name}' is missing provenance field '{required}' (§6)"
78
+ )
79
+ return records
80
+
81
+ # -- gated actions ----------------------------------------------------
82
+ async def request_action(self, name: str, **arguments: Any) -> dict[str, Any]:
83
+ """Step 1: request the action; returns the preview + approval token."""
84
+ result = await self.session.call_tool(name, arguments)
85
+ if result.isError:
86
+ raise HarnessError(self._error_text(result, name))
87
+ payload = result.structuredContent or {}
88
+ if payload.get("status") != "preview":
89
+ raise HarnessError(f"action '{name}' did not return a preview (got {payload!r})")
90
+ return payload
91
+
92
+ async def approve(self, token: str) -> dict[str, Any]:
93
+ """Step 2a: approve and execute by token."""
94
+ result = await self.session.call_tool(EXECUTE_TOOL, {"approval_token": token})
95
+ if result.isError:
96
+ raise HarnessError(self._error_text(result, EXECUTE_TOOL))
97
+ return result.structuredContent or {}
98
+
99
+ async def reject(self, token: str) -> dict[str, Any]:
100
+ """Step 2b: reject by token; the action never runs."""
101
+ result = await self.session.call_tool(REJECT_TOOL, {"approval_token": token})
102
+ if result.isError:
103
+ raise HarnessError(self._error_text(result, REJECT_TOOL))
104
+ return result.structuredContent or {}
105
+
106
+ async def run_action(self, name: str, *, approve: bool, **arguments: Any) -> dict[str, Any]:
107
+ """Drive the full sequence: preview, then approve→execute or reject."""
108
+ preview = await self.request_action(name, **arguments)
109
+ token = preview["approval_token"]
110
+ outcome = await self.approve(token) if approve else await self.reject(token)
111
+ return {"preview": preview, "outcome": outcome}
112
+
113
+ # -- helpers ----------------------------------------------------------
114
+ @staticmethod
115
+ def _error_text(result, name: str) -> str:
116
+ for block in result.content:
117
+ if getattr(block, "text", None):
118
+ return f"tool '{name}' errored: {block.text}"
119
+ return f"tool '{name}' errored"
@@ -0,0 +1,78 @@
1
+ """Connector manifest — identity, capabilities, auth, deployment compatibility.
2
+
3
+ The manifest is what the Connector Infrastructure layer reads to populate the
4
+ admin approval directory and the user-facing connector list. It also declares
5
+ the SDK major version the connector targets, so the gateway can refuse a
6
+ connector built against an incompatible SDK rather than failing at call time.
7
+
8
+ See SDK_GUIDE.md §3 and §11.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from enum import Enum
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+ # Single source of truth for the SDK's version. The package version in
18
+ # pyproject.toml must match SDK_VERSION (guarded by a test). The contract major
19
+ # version — what a connector manifest targets — is the major of SDK_VERSION.
20
+ SDK_VERSION = "1.0.0"
21
+ SDK_MAJOR_VERSION = int(SDK_VERSION.split(".")[0])
22
+
23
+
24
+ class AuthMethod(str, Enum):
25
+ """How a connector authenticates to its external system.
26
+
27
+ Connectors never store credentials — the gateway owns credential lifecycle
28
+ and injects a valid token at call time (SDK_GUIDE.md §7). This enum only
29
+ declares *which* method the connector needs.
30
+ """
31
+
32
+ NONE = "none"
33
+ API_KEY = "api_key"
34
+ OAUTH2 = "oauth2" # OAuth 2.1 authorization-code flow with PKCE
35
+ BASIC = "basic"
36
+ MTLS = "mtls"
37
+ CUSTOM = "custom"
38
+
39
+
40
+ class ResourceManifest(BaseModel):
41
+ name: str
42
+ description: str
43
+ mutates: bool = False # always False for resources; present for uniformity
44
+
45
+
46
+ class ActionManifest(BaseModel):
47
+ name: str
48
+ description: str
49
+ mutates: bool = True # always True for actions
50
+ has_preview: bool = True
51
+
52
+
53
+ class DeploymentCompat(BaseModel):
54
+ """Deployment-mode honesty: a connector must declare its network needs so
55
+ air-gapped deployments can refuse network-dependent connectors."""
56
+
57
+ requires_network: bool = Field(..., description="Does the connector need external network access?")
58
+ air_gapped_capable: bool = Field(..., description="Can it run in an air-gapped deployment?")
59
+
60
+
61
+ class Manifest(BaseModel):
62
+ """The full connector manifest."""
63
+
64
+ name: str
65
+ version: str
66
+ description: str = ""
67
+ sdk_major_version: int = SDK_MAJOR_VERSION
68
+ auth_method: AuthMethod = AuthMethod.NONE
69
+ auth_scopes: list[str] = Field(
70
+ default_factory=list,
71
+ description="OAuth scopes the connector requests; should be least-privilege (§13).",
72
+ )
73
+ resources: list[ResourceManifest] = Field(default_factory=list)
74
+ actions: list[ActionManifest] = Field(default_factory=list)
75
+ deployment: DeploymentCompat
76
+
77
+ def to_json(self, *, indent: int = 2) -> str:
78
+ return self.model_dump_json(indent=indent)
@@ -0,0 +1,5 @@
1
+ """MCP emission layer — wraps the official MCP server SDK."""
2
+
3
+ from .emitter import build_tools, emit_server, run_stdio, serve_stdio
4
+
5
+ __all__ = ["build_tools", "emit_server", "run_stdio", "serve_stdio"]
@@ -0,0 +1,174 @@
1
+ """Emit a compliant MCP server from a `Connector`.
2
+
3
+ This is the layer that makes "everything emits MCP" real (SDK_GUIDE.md §2). It
4
+ wraps the official MCP server SDK's low-level `Server` — it does **not**
5
+ reimplement MCP. Every capability becomes an MCP tool:
6
+
7
+ - Resources are emitted as **read-only** tools (`annotations.readOnlyHint=True`,
8
+ `dq.mutates=False`) so the agent can call them freely during context assembly.
9
+ They return their records wrapped in the provenance envelope.
10
+ - Actions are emitted as tools tagged `dq.mutates=True` /
11
+ `annotations.destructiveHint=True`. Calling an action *requests* it: the server
12
+ runs `preview`, mints a single-use approval token, and returns
13
+ `{status, approval_token, preview, arguments}` without executing. The gateway
14
+ then drives the decision through two control tools:
15
+ `dq.execute_action(approval_token)` and `dq.reject_action(approval_token)`.
16
+
17
+ Reads are modelled as read-only tools (rather than static MCP resources) because
18
+ a connector read takes query parameters from the agent; tools carry an input
19
+ schema, so this is the faithful MCP mapping and keeps `dq.mutates` uniform
20
+ across every capability.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any
26
+
27
+ from mcp import types
28
+ from mcp.server.lowlevel import Server
29
+
30
+ from ..action import ActionSpec
31
+ from ..classification import DQ_CONNECTOR, DQ_KIND, DQ_MUTATES, CapabilityKind, meta_for
32
+ from ..connector import Connector
33
+ from ..resource import ResourceSpec
34
+
35
+ # Names of the two control tools that drive the gated action lifecycle.
36
+ EXECUTE_TOOL = "dq.execute_action"
37
+ REJECT_TOOL = "dq.reject_action"
38
+
39
+ _APPROVAL_SCHEMA = {
40
+ "type": "object",
41
+ "properties": {
42
+ "approval_token": {"type": "string", "description": "Token returned when the action was requested."}
43
+ },
44
+ "required": ["approval_token"],
45
+ }
46
+
47
+
48
+ def _control_meta(connector_name: str, *, mutates: bool) -> dict[str, object]:
49
+ return {DQ_KIND: "control", DQ_MUTATES: mutates, DQ_CONNECTOR: connector_name}
50
+
51
+
52
+ def build_tools(connector: Connector) -> list[types.Tool]:
53
+ """Build the MCP `Tool` list for a connector, with `dq.*` classification."""
54
+ tools: list[types.Tool] = []
55
+
56
+ for spec in connector.resources:
57
+ tools.append(
58
+ types.Tool(
59
+ name=spec.name,
60
+ description=spec.description,
61
+ inputSchema=spec.input_schema,
62
+ annotations=types.ToolAnnotations(readOnlyHint=True, destructiveHint=False),
63
+ _meta=meta_for(CapabilityKind.RESOURCE, connector.name),
64
+ )
65
+ )
66
+
67
+ for spec in connector.actions:
68
+ tools.append(
69
+ types.Tool(
70
+ name=spec.name,
71
+ description=spec.description,
72
+ inputSchema=spec.input_schema,
73
+ annotations=types.ToolAnnotations(readOnlyHint=False, destructiveHint=True),
74
+ _meta=meta_for(CapabilityKind.ACTION, connector.name),
75
+ )
76
+ )
77
+
78
+ # Control tools: only emitted when the connector has at least one action.
79
+ if connector.actions:
80
+ tools.append(
81
+ types.Tool(
82
+ name=EXECUTE_TOOL,
83
+ description="Execute a previously previewed action, after human approval, by its approval token.",
84
+ inputSchema=_APPROVAL_SCHEMA,
85
+ annotations=types.ToolAnnotations(readOnlyHint=False, destructiveHint=True),
86
+ _meta=_control_meta(connector.name, mutates=True),
87
+ )
88
+ )
89
+ tools.append(
90
+ types.Tool(
91
+ name=REJECT_TOOL,
92
+ description="Reject a previously previewed action by its approval token; it is never executed.",
93
+ inputSchema=_APPROVAL_SCHEMA,
94
+ annotations=types.ToolAnnotations(readOnlyHint=True, destructiveHint=False),
95
+ _meta=_control_meta(connector.name, mutates=False),
96
+ )
97
+ )
98
+
99
+ return tools
100
+
101
+
102
+ def emit_server(connector: Connector) -> Server:
103
+ """Produce a low-level MCP `Server` backed by the connector."""
104
+ server: Server = Server(name=connector.name, version=connector.version)
105
+
106
+ resources_by_name: dict[str, ResourceSpec] = {s.name: s for s in connector.resources}
107
+ actions_by_name: dict[str, ActionSpec] = {s.name: s for s in connector.actions}
108
+
109
+ @server.list_tools()
110
+ async def _list_tools() -> list[types.Tool]:
111
+ return build_tools(connector)
112
+
113
+ @server.call_tool()
114
+ async def _call_tool(name: str, arguments: dict[str, Any]) -> Any:
115
+ args = arguments or {}
116
+
117
+ if name in resources_by_name:
118
+ records = connector.fetch_resource(name, args)
119
+ # Structured content: the provenance-wrapped records. The MCP layer
120
+ # also serialises this to JSON text in `content` for generic clients.
121
+ return {"records": [r.model_dump(mode="json") for r in records]}
122
+
123
+ if name in actions_by_name:
124
+ # Requesting an action previews it and mints a token; it does NOT run.
125
+ pending = connector.request_action(name, args)
126
+ return {
127
+ "status": "preview",
128
+ "action": pending.action_name,
129
+ "approval_token": pending.token,
130
+ "preview": pending.preview,
131
+ "arguments": pending.arguments,
132
+ "dq_mutates": True,
133
+ "next": {
134
+ "approve": f"call {EXECUTE_TOOL} with this approval_token",
135
+ "reject": f"call {REJECT_TOOL} with this approval_token",
136
+ },
137
+ }
138
+
139
+ if name == EXECUTE_TOOL:
140
+ token = args["approval_token"]
141
+ result = connector.execute_approved(token)
142
+ return {"status": "executed", "result": _jsonable(result)}
143
+
144
+ if name == REJECT_TOOL:
145
+ token = args["approval_token"]
146
+ pending = connector.reject_action(token)
147
+ return {"status": "rejected", "action": pending.action_name}
148
+
149
+ raise ValueError(f"unknown tool: {name!r}")
150
+
151
+ return server
152
+
153
+
154
+ def _jsonable(value: Any) -> Any:
155
+ """Best-effort conversion of an execute() return into JSON-safe content."""
156
+ if hasattr(value, "model_dump"):
157
+ return value.model_dump(mode="json")
158
+ return value
159
+
160
+
161
+ async def serve_stdio(connector: Connector) -> None:
162
+ """Serve the connector's MCP server over stdio (async)."""
163
+ from mcp.server.stdio import stdio_server
164
+
165
+ server = emit_server(connector)
166
+ async with stdio_server() as (read_stream, write_stream):
167
+ await server.run(read_stream, write_stream, server.create_initialization_options())
168
+
169
+
170
+ def run_stdio(connector: Connector) -> None:
171
+ """Blocking entry point: serve the connector over stdio until disconnected."""
172
+ import anyio
173
+
174
+ anyio.run(serve_stdio, connector)
@@ -0,0 +1,65 @@
1
+ """Provenance contract — how live data stays citeable.
2
+
3
+ Live connector data is never ingested or persisted. It enters an agent's
4
+ context for a single query, gets cited, and is discarded. For that to preserve
5
+ groundedness, every record a resource returns must carry enough provenance to
6
+ build an honest, verifiable citation.
7
+
8
+ See SDK_GUIDE.md §6.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ from pydantic import BaseModel, Field
17
+
18
+
19
+ def now_iso() -> str:
20
+ """Current UTC time as an ISO 8601 string (the `retrieved_at` stamp)."""
21
+ return datetime.now(timezone.utc).isoformat()
22
+
23
+
24
+ class Provenance(BaseModel):
25
+ """The required envelope wrapping every record a resource returns.
26
+
27
+ `retrieved_at` is not decoration: a document citation is stable forever, but
28
+ a live citation is a snapshot of a moving target. The timestamp is what lets
29
+ an answer say "Jira DQ-431, status 'In Review', as of 2026-06-06 14:32" —
30
+ true at that instant and honest that it may not be true later.
31
+ """
32
+
33
+ connector_name: str = Field(..., description="Which connector produced this record.")
34
+ source_object_id: str = Field(..., description="Stable ID of the source object, e.g. 'DQ-431'.")
35
+ retrieved_at: str = Field(default_factory=now_iso, description="ISO 8601 retrieval timestamp.")
36
+ title_or_label: str = Field(..., description="Human-readable label for the citation.")
37
+ deep_link: str | None = Field(default=None, description="Direct URL to the source object, when available.")
38
+ mutability_note: str | None = Field(default=None, description="Hint that this data can change, e.g. 'live status field'.")
39
+
40
+
41
+ class CitedRecord(BaseModel):
42
+ """A single record returned by a resource: the payload plus its provenance."""
43
+
44
+ data: Any = Field(..., description="The record payload the agent will read.")
45
+ provenance: Provenance
46
+
47
+
48
+ def make_provenance(
49
+ *,
50
+ connector_name: str,
51
+ source_object_id: str,
52
+ title_or_label: str,
53
+ deep_link: str | None = None,
54
+ mutability_note: str | None = None,
55
+ retrieved_at: str | None = None,
56
+ ) -> Provenance:
57
+ """Build a `Provenance` envelope, stamping `retrieved_at` to now if omitted."""
58
+ return Provenance(
59
+ connector_name=connector_name,
60
+ source_object_id=source_object_id,
61
+ retrieved_at=retrieved_at or now_iso(),
62
+ title_or_label=title_or_label,
63
+ deep_link=deep_link,
64
+ mutability_note=mutability_note,
65
+ )
@@ -0,0 +1,65 @@
1
+ """Resource definitions — the read side of a connector.
2
+
3
+ A resource is a read-only capability. It is declared with the `@resource`
4
+ decorator on a method of a `Connector` subclass. Resources are exempt from the
5
+ approval gate; the SDK enforces read-only classification structurally by giving
6
+ reads and actions different decorators that cannot be confused.
7
+
8
+ See SDK_GUIDE.md §4.2.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Callable
14
+
15
+ from .classification import CapabilityKind
16
+
17
+ # Marker attribute the Connector base class scans for at subclass-definition time.
18
+ _RESOURCE_MARKER = "__dq_resource__"
19
+
20
+
21
+ class ResourceSpec:
22
+ """Declarative metadata for one resource, attached to its fetch function."""
23
+
24
+ kind = CapabilityKind.RESOURCE
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ name: str,
30
+ description: str,
31
+ input_schema: dict[str, Any],
32
+ attr_name: str,
33
+ ) -> None:
34
+ self.name = name
35
+ self.description = description
36
+ self.input_schema = input_schema
37
+ self.attr_name = attr_name # method name on the connector, used to bind at emit time
38
+
39
+ def __repr__(self) -> str: # pragma: no cover - debug aid
40
+ return f"ResourceSpec(name={self.name!r})"
41
+
42
+
43
+ def resource(
44
+ *,
45
+ description: str,
46
+ name: str | None = None,
47
+ input_schema: dict[str, Any] | None = None,
48
+ ) -> Callable[[Callable], Callable]:
49
+ """Declare a read-only resource.
50
+
51
+ The decorated method runs the search/fetch and must return an iterable of
52
+ `CitedRecord` (use `self.cite(...)` to build them). The `description` is read
53
+ by the agent's planner, so it should explain in natural language when to use
54
+ this resource. (It is also a prompt-injection surface — see SDK_GUIDE.md §13.)
55
+ """
56
+
57
+ def deco(fn: Callable) -> Callable:
58
+ fn.__dq_resource__ = {
59
+ "name": name or fn.__name__,
60
+ "description": description,
61
+ "input_schema": input_schema or {"type": "object", "properties": {}},
62
+ }
63
+ return fn
64
+
65
+ return deco
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .env
@@ -0,0 +1,22 @@
1
+ # __CONNECTOR_TITLE__ connector
2
+
3
+ A Deep Query connector built with [DeepQuerySDK](https://deepquery.local/sdk).
4
+
5
+ ## Develop
6
+
7
+ ```bash
8
+ deepquery validate connector.py # check the safety contracts (§5, §6, §13)
9
+ deepquery manifest connector.py # print the connector manifest
10
+ deepquery run-dev connector.py # drive it with the mock agent locally
11
+ deepquery emit connector.py --out dist/ # produce the deployable MCP server artifact
12
+ ```
13
+
14
+ ## What to fill in
15
+
16
+ 1. **Auth** — set the real endpoints/scopes (or switch strategy). Request the
17
+ minimum scopes you need.
18
+ 2. **`search`** — call your system's read API; wrap each record with `self.cite(...)`.
19
+ 3. **`do_thing`** — implement `preview` (describe the effect) and `execute` (perform it).
20
+
21
+ Resources are read-only and ungated; actions always pass through
22
+ preview → approve → execute. `validate` enforces both.