ocp-client 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,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,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"]
|
|
@@ -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,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ocp-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Open Context Protocol — Python client SDK"
|
|
5
|
+
license = { text = "Apache-2.0" }
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mcp>=1.0",
|
|
9
|
+
"pydantic>=2.7",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/Rajesh1213/OCP"
|
|
14
|
+
Repository = "https://github.com/Rajesh1213/OCP"
|
|
15
|
+
Documentation = "https://github.com/Rajesh1213/OCP/blob/main/docs/integrations.md"
|
|
16
|
+
"Bug Tracker" = "https://github.com/Rajesh1213/OCP/issues"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["ocp_client"]
|