ocp-client 0.1.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.
ocp_client/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """OCP Python client SDK."""
2
+ from .client import OCPClient
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["OCPClient"]
ocp_client/client.py ADDED
@@ -0,0 +1,232 @@
1
+ """OCP client — async, typed wrapper over MCP tool calls."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from contextlib import asynccontextmanager
6
+ from typing import Any, AsyncIterator
7
+
8
+ from mcp import ClientSession, StdioServerParameters
9
+ from mcp.client.stdio import stdio_client
10
+ from mcp.types import TextContent
11
+
12
+ from ocp_client.types import (
13
+ Chunk,
14
+ CheckpointResult,
15
+ HandoffResult,
16
+ IndexResult,
17
+ InvalidateResult,
18
+ OCPError,
19
+ PackResult,
20
+ SearchResult,
21
+ SessionResult,
22
+ SetResult,
23
+ StateEntry,
24
+ SubscriptionResult,
25
+ WorkspaceRegistered,
26
+ )
27
+
28
+
29
+ class OCPClient:
30
+ """High-level async client for an OCP server.
31
+
32
+ Usage::
33
+
34
+ async with OCPClient.stdio(["ocp-server"]) as client:
35
+ ws = await client.workspace_register("file:///my/repo")
36
+ await client.workspace_index(ws.workspace_id)
37
+ results = await client.context_search(ws.workspace_id, "auth middleware")
38
+ """
39
+
40
+ def __init__(self, session: ClientSession) -> None:
41
+ self._session = session
42
+
43
+ # ------------------------------------------------------------------ #
44
+ # Factory #
45
+ # ------------------------------------------------------------------ #
46
+
47
+ @staticmethod
48
+ @asynccontextmanager
49
+ async def stdio(command: list[str], env: dict[str, str] | None = None) -> AsyncIterator["OCPClient"]:
50
+ params = StdioServerParameters(command=command[0], args=command[1:], env=env)
51
+ async with stdio_client(params) as (read, write):
52
+ async with ClientSession(read, write) as session:
53
+ await session.initialize()
54
+ yield OCPClient(session)
55
+
56
+ # ------------------------------------------------------------------ #
57
+ # Internal #
58
+ # ------------------------------------------------------------------ #
59
+
60
+ async def _call(self, tool: str, **kwargs: Any) -> Any:
61
+ args = {k: v for k, v in kwargs.items() if v is not None}
62
+ result = await self._session.call_tool(tool, args)
63
+ if not result.content:
64
+ return {}
65
+ item = result.content[0]
66
+ if not isinstance(item, TextContent):
67
+ return {}
68
+ raw = item.text
69
+ data = json.loads(raw)
70
+ if "error" in data:
71
+ raise OCPError(data["error"]["code"], data["error"]["message"])
72
+ return data
73
+
74
+ # ------------------------------------------------------------------ #
75
+ # Workspace — §4.1 #
76
+ # ------------------------------------------------------------------ #
77
+
78
+ async def workspace_register(
79
+ self, root_uri: str, name: str | None = None, metadata: dict | None = None
80
+ ) -> WorkspaceRegistered:
81
+ data = await self._call("workspace.register", root_uri=root_uri, name=name, metadata=metadata)
82
+ return WorkspaceRegistered(**data)
83
+
84
+ async def workspace_index(
85
+ self, workspace_id: str, paths: list[str] | None = None, wait: bool | None = None
86
+ ) -> IndexResult:
87
+ data = await self._call("workspace.index", workspace_id=workspace_id, paths=paths, wait=wait)
88
+ return IndexResult(**data)
89
+
90
+ async def workspace_invalidate(self, workspace_id: str, paths: list[str]) -> InvalidateResult:
91
+ data = await self._call("workspace.invalidate", workspace_id=workspace_id, paths=paths)
92
+ return InvalidateResult(**data)
93
+
94
+ async def workspace_list_chunks(
95
+ self, workspace_id: str, filters: dict | None = None, cursor: str | None = None
96
+ ) -> tuple[list[Chunk], str | None]:
97
+ data = await self._call("workspace.list_chunks", workspace_id=workspace_id, filters=filters, cursor=cursor)
98
+ return [Chunk(**c) for c in data["chunks"]], data.get("next_cursor")
99
+
100
+ # ------------------------------------------------------------------ #
101
+ # Retrieval — §4.2 #
102
+ # ------------------------------------------------------------------ #
103
+
104
+ async def context_search(
105
+ self, workspace_id: str, query: str, k: int = 5, filters: dict | None = None
106
+ ) -> SearchResult:
107
+ data = await self._call("context.search", workspace_id=workspace_id, query=query, k=k, filters=filters)
108
+ return SearchResult(chunks=[Chunk(**c) for c in data["chunks"]], scores=data["scores"])
109
+
110
+ async def context_get_chunk(self, chunk_id: str) -> Chunk:
111
+ data = await self._call("context.get_chunk", chunk_id=chunk_id)
112
+ return Chunk(**data["chunk"])
113
+
114
+ async def context_pack(
115
+ self, workspace_id: str, intent: str, budget_tokens: int, include_state: bool = False
116
+ ) -> PackResult:
117
+ data = await self._call(
118
+ "context.pack", workspace_id=workspace_id, intent=intent,
119
+ budget_tokens=budget_tokens, include_state=include_state,
120
+ )
121
+ return PackResult(**data)
122
+
123
+ # ------------------------------------------------------------------ #
124
+ # State — §4.3 #
125
+ # ------------------------------------------------------------------ #
126
+
127
+ async def state_set(
128
+ self, key: str, value: Any, scope: str,
129
+ workspace_id: str | None = None, session_id: str | None = None,
130
+ agent_id: str | None = None, ttl_seconds: int | None = None,
131
+ if_version: int | None = None,
132
+ ) -> SetResult:
133
+ data = await self._call(
134
+ "state.set", key=key, value=value, scope=scope,
135
+ workspace_id=workspace_id, session_id=session_id,
136
+ agent_id=agent_id, ttl_seconds=ttl_seconds, if_version=if_version,
137
+ )
138
+ return SetResult(**data)
139
+
140
+ async def state_get(
141
+ self, key: str, scope: str | None = None,
142
+ workspace_id: str | None = None, session_id: str | None = None,
143
+ agent_id: str | None = None,
144
+ ) -> StateEntry | None:
145
+ data = await self._call(
146
+ "state.get", key=key, scope=scope,
147
+ workspace_id=workspace_id, session_id=session_id, agent_id=agent_id,
148
+ )
149
+ entry = data.get("entry")
150
+ return StateEntry(**entry) if entry else None
151
+
152
+ async def state_list(
153
+ self, prefix: str | None = None, scope: str | None = None,
154
+ workspace_id: str | None = None, session_id: str | None = None,
155
+ agent_id: str | None = None, cursor: str | None = None,
156
+ ) -> tuple[list[StateEntry], str | None]:
157
+ data = await self._call(
158
+ "state.list", prefix=prefix, scope=scope,
159
+ workspace_id=workspace_id, session_id=session_id,
160
+ agent_id=agent_id, cursor=cursor,
161
+ )
162
+ return [StateEntry(**e) for e in data["entries"]], data.get("next_cursor")
163
+
164
+ async def state_delete(
165
+ self, key: str, scope: str,
166
+ workspace_id: str | None = None, session_id: str | None = None,
167
+ agent_id: str | None = None, if_version: int | None = None,
168
+ ) -> bool:
169
+ data = await self._call(
170
+ "state.delete", key=key, scope=scope,
171
+ workspace_id=workspace_id, session_id=session_id,
172
+ agent_id=agent_id, if_version=if_version,
173
+ )
174
+ return data["deleted"]
175
+
176
+ # ------------------------------------------------------------------ #
177
+ # Coordination — §4.4 #
178
+ # ------------------------------------------------------------------ #
179
+
180
+ async def session_open(
181
+ self, workspace_id: str, session_id: str | None = None,
182
+ ttl_seconds: int | None = None, metadata: dict | None = None,
183
+ ) -> SessionResult:
184
+ data = await self._call(
185
+ "session.open", workspace_id=workspace_id, session_id=session_id,
186
+ ttl_seconds=ttl_seconds, metadata=metadata,
187
+ )
188
+ return SessionResult(**data)
189
+
190
+ async def session_close(self, session_id: str) -> bool:
191
+ data = await self._call("session.close", session_id=session_id)
192
+ return data["closed"]
193
+
194
+ async def session_handoff(
195
+ self, session_id: str, from_agent: str, to_agent: str, message: Any
196
+ ) -> HandoffResult:
197
+ data = await self._call(
198
+ "session.handoff", session_id=session_id,
199
+ from_agent=from_agent, to_agent=to_agent, message=message,
200
+ )
201
+ return HandoffResult(**data)
202
+
203
+ async def session_checkpoint(
204
+ self, session_id: str, label: str, include_state: bool = False
205
+ ) -> CheckpointResult:
206
+ data = await self._call(
207
+ "session.checkpoint", session_id=session_id,
208
+ label=label, include_state=include_state,
209
+ )
210
+ return CheckpointResult(**data)
211
+
212
+ async def session_restore(self, checkpoint_id: str) -> SessionResult:
213
+ data = await self._call("session.restore", checkpoint_id=checkpoint_id)
214
+ return SessionResult(**data)
215
+
216
+ # ------------------------------------------------------------------ #
217
+ # Events — §4.5 #
218
+ # ------------------------------------------------------------------ #
219
+
220
+ async def events_subscribe(
221
+ self, workspace_id: str, types: list[str] | None = None,
222
+ session_id: str | None = None, since: str | None = None,
223
+ ) -> SubscriptionResult:
224
+ data = await self._call(
225
+ "events.subscribe", workspace_id=workspace_id,
226
+ types=types, session_id=session_id, since=since,
227
+ )
228
+ return SubscriptionResult(**data)
229
+
230
+ async def events_unsubscribe(self, subscription_id: str) -> bool:
231
+ data = await self._call("events.unsubscribe", subscription_id=subscription_id)
232
+ return data["unsubscribed"]
ocp_client/types.py ADDED
@@ -0,0 +1,96 @@
1
+ """Typed wrappers for OCP responses."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class WorkspaceRegistered(BaseModel):
9
+ workspace_id: str
10
+ created: bool
11
+
12
+
13
+ class IndexResult(BaseModel):
14
+ indexed: int
15
+ skipped: int
16
+ duration_ms: int
17
+
18
+
19
+ class InvalidateResult(BaseModel):
20
+ invalidated: int
21
+
22
+
23
+ class SourceRange(BaseModel):
24
+ start_byte: int | None = None
25
+ end_byte: int | None = None
26
+ start_line: int | None = None
27
+ end_line: int | None = None
28
+
29
+
30
+ class ChunkSource(BaseModel):
31
+ uri: str
32
+ range: SourceRange | None = None
33
+ content_hash: str
34
+
35
+
36
+ class Chunk(BaseModel):
37
+ id: str
38
+ workspace_id: str
39
+ source: ChunkSource
40
+ kind: str
41
+ language: str | None = None
42
+ symbol: str | None = None
43
+ content: str
44
+ metadata: dict[str, Any] = {}
45
+ version: int = 1
46
+
47
+
48
+ class SearchResult(BaseModel):
49
+ chunks: list[Chunk]
50
+ scores: list[float]
51
+
52
+
53
+ class PackResult(BaseModel):
54
+ context: str
55
+ chunks_used: list[str]
56
+ state_used: list[str]
57
+ tokens: int
58
+
59
+
60
+ class StateEntry(BaseModel):
61
+ key: str
62
+ value: Any
63
+ scope: str
64
+ workspace_id: str | None = None
65
+ session_id: str | None = None
66
+ agent_id: str | None = None
67
+ ttl_seconds: int | None = None
68
+ updated_at: str | None = None
69
+ version: int = 1
70
+
71
+
72
+ class SetResult(BaseModel):
73
+ version: int
74
+
75
+
76
+ class SessionResult(BaseModel):
77
+ session_id: str
78
+
79
+
80
+ class HandoffResult(BaseModel):
81
+ delivered: bool
82
+ handoff_id: str
83
+
84
+
85
+ class CheckpointResult(BaseModel):
86
+ checkpoint_id: str
87
+
88
+
89
+ class SubscriptionResult(BaseModel):
90
+ subscription_id: str
91
+
92
+
93
+ class OCPError(Exception):
94
+ def __init__(self, code: str, message: str) -> None:
95
+ self.code = code
96
+ super().__init__(f"[{code}] {message}")
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: ocp-client
3
+ Version: 0.1.0
4
+ Summary: Open Context Protocol — Python client SDK
5
+ Project-URL: Homepage, https://github.com/Rajesh1213/OCP
6
+ Project-URL: Repository, https://github.com/Rajesh1213/OCP
7
+ Project-URL: Documentation, https://github.com/Rajesh1213/OCP/blob/main/docs/integrations.md
8
+ Project-URL: Bug Tracker, https://github.com/Rajesh1213/OCP/issues
9
+ License: Apache-2.0
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: mcp>=1.0
12
+ Requires-Dist: pydantic>=2.7
@@ -0,0 +1,6 @@
1
+ ocp_client/__init__.py,sha256=oWrUgLOYZQcgTk8Q71C3mdJLiQVDC8bkD94IAbQWCpE,106
2
+ ocp_client/client.py,sha256=v10SyCyF_hP7mtnVx8vohCwen-3kz-0W5QCX547Gd6U,9729
3
+ ocp_client/types.py,sha256=os5kC431LRPl61P-Oru430bl-uTwakvdl0gK8ttaQm4,1777
4
+ ocp_client-0.1.0.dist-info/METADATA,sha256=AxJWfePw_LZuYAKnYCduaB1YMVrKBWg_Je_-M0yelEM,480
5
+ ocp_client-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ ocp_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any