ragleap-integrations 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.
- ragleap_integrations-0.1.0/.gitignore +32 -0
- ragleap_integrations-0.1.0/CHANGELOG.md +53 -0
- ragleap_integrations-0.1.0/PKG-INFO +156 -0
- ragleap_integrations-0.1.0/README.md +131 -0
- ragleap_integrations-0.1.0/docs/design/mcp-client.md +205 -0
- ragleap_integrations-0.1.0/pyproject.toml +39 -0
- ragleap_integrations-0.1.0/src/ragleap_integrations/__init__.py +30 -0
- ragleap_integrations-0.1.0/src/ragleap_integrations/_net.py +268 -0
- ragleap_integrations-0.1.0/src/ragleap_integrations/mcp.py +702 -0
- ragleap_integrations-0.1.0/tests/test_mcp.py +875 -0
- ragleap_integrations-0.1.0/tests/test_net.py +440 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Environment
|
|
2
|
+
.env
|
|
3
|
+
*.env.local
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
venv/
|
|
10
|
+
.venv/
|
|
11
|
+
|
|
12
|
+
# Node
|
|
13
|
+
node_modules/
|
|
14
|
+
|
|
15
|
+
# Docker
|
|
16
|
+
docker-compose.override.yml
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# Logs
|
|
27
|
+
*.log
|
|
28
|
+
.env
|
|
29
|
+
|
|
30
|
+
# Package build artifacts
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `ragleap-integrations` are documented here. Format
|
|
4
|
+
loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-10-05
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- An MCP client over Streamable HTTP that returns `ragleap_tools.Tool`
|
|
11
|
+
objects (`McpConfig`, `McpServerConfig`, `McpToolSpec`, `McpClient`,
|
|
12
|
+
`make_mcp_tools`). Owner-configured servers and an exact `server.tool`
|
|
13
|
+
allowlist; nothing is read from the environment; tools are discovered once
|
|
14
|
+
(a snapshot) or supplied by the owner as specs, in which case nothing is
|
|
15
|
+
sent over the network at setup.
|
|
16
|
+
- Modern (2026-07-28) requests with `Mcp-Method`, `Mcp-Name` and
|
|
17
|
+
`Mcp-Param-*` headers (Base64 sentinel encoding where required) and
|
|
18
|
+
`params._meta`; detection of legacy servers per the specification's
|
|
19
|
+
backward-compatibility rules, with a legacy `initialize` fallback for
|
|
20
|
+
2025-06-18 only.
|
|
21
|
+
- A bounded HTTPS transport using only the standard library: public-address
|
|
22
|
+
check with the connection pinned to the resolved IP, TLS verified against the
|
|
23
|
+
hostname, no redirects, a response-size cap, constant error messages, and a
|
|
24
|
+
wall-clock deadline enforced by a watchdog (see
|
|
25
|
+
`docs/design/mcp-client.md` for the slow-drip experiment behind it).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Found by the live check, before release: a legacy server that rejects the
|
|
30
|
+
modern request with its own JSON-RPC error (DeepWiki answered HTTP 400 and
|
|
31
|
+
`-32600`) was reported as an error instead of triggering the legacy
|
|
32
|
+
fallback. Only `-32020`, `-32021` and `-32022` are treated as recognized
|
|
33
|
+
modern errors now, and a failed fallback still reports the first reply's
|
|
34
|
+
code.
|
|
35
|
+
|
|
36
|
+
### Verified
|
|
37
|
+
|
|
38
|
+
- 201 tests, including the real-TLS transport tests described in the README.
|
|
39
|
+
- Live-checked on 2026-10-05 against DeepWiki's public MCP server (legacy path;
|
|
40
|
+
see the README's Verification status for exactly what that covers).
|
|
41
|
+
|
|
42
|
+
### Not verified
|
|
43
|
+
|
|
44
|
+
- The modern 2026-07-28 path against a real server, `x-mcp-header`
|
|
45
|
+
mirroring, the location of a `-32022` error's supported-version list,
|
|
46
|
+
bearer-token authentication, plain JSON responses and pagination.
|
|
47
|
+
|
|
48
|
+
### Known limitations
|
|
49
|
+
|
|
50
|
+
- On a legacy server every call performs a full `initialize` handshake and
|
|
51
|
+
never closes the session. `HeaderMismatch` is not retried. Only text
|
|
52
|
+
content is returned. Server-supplied text (descriptions, schemas, results)
|
|
53
|
+
is untrusted and not screened for prompt injection.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ragleap-integrations
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP client that turns allowlisted tools on remote MCP servers into ragleap-tools Tool objects. Streamable HTTP only, standard library plus ragleap-tools, owner-configured allowlist, pinned and bounded network access. BYOK, no vendor lock-in.
|
|
5
|
+
Project-URL: Homepage, https://github.com/antonyrag/ragleap-core
|
|
6
|
+
Project-URL: Repository, https://github.com/antonyrag/ragleap-core
|
|
7
|
+
Project-URL: Documentation, https://github.com/antonyrag/ragleap-core/tree/main/packages/ragleap-integrations
|
|
8
|
+
Project-URL: Issues, https://github.com/antonyrag/ragleap-core/issues
|
|
9
|
+
Author-email: Antony <antony@ragleap.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: agents,byok,integrations,llm,mcp,model-context-protocol,self-hosted,tool-calling
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: ragleap-tools>=0.4.0
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# ragleap-integrations
|
|
27
|
+
|
|
28
|
+
An MCP (Model Context Protocol) client that turns allowlisted tools on remote
|
|
29
|
+
MCP servers into `ragleap_tools.Tool` objects, so MCP tools and native tools
|
|
30
|
+
share one shape.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ragleap-integrations
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What this is (and isn't)
|
|
37
|
+
|
|
38
|
+
v0.1.0 is an MCP client over **Streamable HTTP only**. The *owner* configures
|
|
39
|
+
servers and an exact `server.tool` allowlist; the model only supplies a tool's
|
|
40
|
+
JSON arguments, never a URL, a token or a tool outside the allowlist. It does
|
|
41
|
+
not own a tool-calling loop (that is `ragleap-agents`' job).
|
|
42
|
+
|
|
43
|
+
Not supported in this version: stdio (it launches a subprocess), the
|
|
44
|
+
deprecated HTTP+SSE transport, sampling/elicitation/roots (a server asking
|
|
45
|
+
for client input gets a clear "unsupported" result), resources, prompts,
|
|
46
|
+
subscriptions and OAuth. Connectors, code execution and HTTP fetch are
|
|
47
|
+
deliberately not here: each needs its own security design pass.
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from ragleap_integrations import McpConfig, McpServerConfig, make_mcp_tools
|
|
53
|
+
|
|
54
|
+
config = McpConfig(
|
|
55
|
+
servers=[McpServerConfig("deepwiki", "https://mcp.deepwiki.com/mcp")], # token="..." if a server needs one
|
|
56
|
+
allowed_tools=["deepwiki.read_wiki_structure"],
|
|
57
|
+
)
|
|
58
|
+
tools = make_mcp_tools(config) # discovers the allowlisted tools once (tools/list)
|
|
59
|
+
|
|
60
|
+
openai_tools = [t.to_openai_schema() for t in tools] # or t.to_gemini_schema()
|
|
61
|
+
result = tools[0].call(repoName="sqlite/sqlite")
|
|
62
|
+
print(result.success, result.result) # True {"text": "..."}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Nothing is read from the environment: your application builds the config.
|
|
66
|
+
Tools are exposed as `<server>__<tool>` (characters outside letters, digits,
|
|
67
|
+
`_` and `-` become `_`, at most 64 characters).
|
|
68
|
+
|
|
69
|
+
To keep server-controlled text out of the model's context entirely, give the
|
|
70
|
+
descriptions and schemas yourself; nothing is sent over the network at setup:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from ragleap_integrations import McpToolSpec
|
|
74
|
+
|
|
75
|
+
spec = McpToolSpec(
|
|
76
|
+
server="deepwiki", name="read_wiki_structure",
|
|
77
|
+
description="List the documentation topics of a GitHub repository.",
|
|
78
|
+
parameters={"type": "object", "properties": {"repoName": {"type": "string"}}, "required": ["repoName"]},
|
|
79
|
+
)
|
|
80
|
+
tools = make_mcp_tools(config, specs=[spec])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Trust model
|
|
84
|
+
|
|
85
|
+
- Everything a server says is untrusted text: tool descriptions, input
|
|
86
|
+
schemas and results flow into a model's context. This package does **not**
|
|
87
|
+
screen them for prompt injection. The structural limits are the allowlist,
|
|
88
|
+
a snapshot of the tool list taken once at setup (a server that changes a
|
|
89
|
+
description later has no effect), length caps, and optional owner-supplied
|
|
90
|
+
specs.
|
|
91
|
+
- Server-supplied error text is never put in a result; failures carry a
|
|
92
|
+
constant message plus, at most, an HTTP status or a JSON-RPC error code.
|
|
93
|
+
- Network access is bounded in code: https only, no credentials in the URL,
|
|
94
|
+
no IP-literal hosts, DNS resolved once with every address required to be
|
|
95
|
+
public (so DNS rebinding cannot swap in an internal address), the
|
|
96
|
+
connection pinned to that IP with TLS (minimum 1.2) verified against the
|
|
97
|
+
hostname, no redirects, no compressed responses, a response-size cap, and a
|
|
98
|
+
wall-clock deadline that also covers the TLS handshake and the headers.
|
|
99
|
+
- A bearer token, if configured, is sent only to its own server URL and is
|
|
100
|
+
never logged.
|
|
101
|
+
|
|
102
|
+
## Protocol
|
|
103
|
+
|
|
104
|
+
The current revision (2026-07-28) is tried first: every request is its own
|
|
105
|
+
POST carrying `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers and
|
|
106
|
+
the version, client info and capabilities in `params._meta`; parameters marked
|
|
107
|
+
`x-mcp-header` in a tool's schema are mirrored into `Mcp-Param-*` headers.
|
|
108
|
+
A 4xx without a recognized modern error (`-32020`, `-32021`, `-32022`)
|
|
109
|
+
identifies a legacy server, and the client then uses the legacy `initialize`
|
|
110
|
+
handshake with protocol version 2025-06-18 only (an explicit allowlist, not
|
|
111
|
+
"whatever the server answers"). The era found is remembered per server.
|
|
112
|
+
|
|
113
|
+
## Limits and defaults
|
|
114
|
+
|
|
115
|
+
| Setting | Default |
|
|
116
|
+
|---|---|
|
|
117
|
+
| Whole-call deadline (`total_timeout`) | 30 s |
|
|
118
|
+
| Per-operation socket timeout (`op_timeout`) | 10 s |
|
|
119
|
+
| Response size (`max_response_bytes`) | 256 KiB |
|
|
120
|
+
| Result text (`max_result_chars`) | 4000 characters |
|
|
121
|
+
| Tool description (`max_description_chars`) | 500 characters |
|
|
122
|
+
| Input schema (`max_schema_chars`) | 10000 characters (larger: tool skipped) |
|
|
123
|
+
|
|
124
|
+
## Known limitations
|
|
125
|
+
|
|
126
|
+
- On a legacy server every tool call performs a full `initialize` handshake
|
|
127
|
+
(three POSTs) and never closes the session.
|
|
128
|
+
- A `HeaderMismatch` error is reported, not retried after re-reading the
|
|
129
|
+
tool list.
|
|
130
|
+
- Only text content is returned; other content types are counted and omitted.
|
|
131
|
+
- `ragleap_tools.Tool.call` in ragleap-tools 0.4.0 cannot take an argument
|
|
132
|
+
literally named `self`.
|
|
133
|
+
- Not safe to share one client between threads while calls are running.
|
|
134
|
+
|
|
135
|
+
## Verification status
|
|
136
|
+
|
|
137
|
+
- 201 tests, including a real local TLS server with a throwaway certificate
|
|
138
|
+
for the transport (hostname verification, IP pinning, size cap, truncated
|
|
139
|
+
bodies, and the deadline against slow-drip bodies, slow-drip headers and a
|
|
140
|
+
stalled handshake).
|
|
141
|
+
- **Live-checked on 2026-10-05** against DeepWiki's public MCP server
|
|
142
|
+
(`https://mcp.deepwiki.com/mcp`, no authentication): the server answered
|
|
143
|
+
the modern request with HTTP 400 and a JSON-RPC `-32600` error, the client
|
|
144
|
+
fell back to the legacy handshake, negotiated 2025-06-18, listed tools and
|
|
145
|
+
read a repository's documentation structure. This verifies the legacy path,
|
|
146
|
+
the fallback decision, event-stream responses and the pinned TLS transport
|
|
147
|
+
against a real server.
|
|
148
|
+
- **Not live-verified:** the modern 2026-07-28 path, `x-mcp-header`
|
|
149
|
+
mirroring, where a `-32022` error carries its supported-version list,
|
|
150
|
+
bearer-token authentication, plain JSON (non-stream) responses and
|
|
151
|
+
pagination. They follow the public specification and are covered by tests
|
|
152
|
+
against fakes only; treat them as best-effort until confirmed live.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# ragleap-integrations
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) client that turns allowlisted tools on remote
|
|
4
|
+
MCP servers into `ragleap_tools.Tool` objects, so MCP tools and native tools
|
|
5
|
+
share one shape.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ragleap-integrations
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What this is (and isn't)
|
|
12
|
+
|
|
13
|
+
v0.1.0 is an MCP client over **Streamable HTTP only**. The *owner* configures
|
|
14
|
+
servers and an exact `server.tool` allowlist; the model only supplies a tool's
|
|
15
|
+
JSON arguments, never a URL, a token or a tool outside the allowlist. It does
|
|
16
|
+
not own a tool-calling loop (that is `ragleap-agents`' job).
|
|
17
|
+
|
|
18
|
+
Not supported in this version: stdio (it launches a subprocess), the
|
|
19
|
+
deprecated HTTP+SSE transport, sampling/elicitation/roots (a server asking
|
|
20
|
+
for client input gets a clear "unsupported" result), resources, prompts,
|
|
21
|
+
subscriptions and OAuth. Connectors, code execution and HTTP fetch are
|
|
22
|
+
deliberately not here: each needs its own security design pass.
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from ragleap_integrations import McpConfig, McpServerConfig, make_mcp_tools
|
|
28
|
+
|
|
29
|
+
config = McpConfig(
|
|
30
|
+
servers=[McpServerConfig("deepwiki", "https://mcp.deepwiki.com/mcp")], # token="..." if a server needs one
|
|
31
|
+
allowed_tools=["deepwiki.read_wiki_structure"],
|
|
32
|
+
)
|
|
33
|
+
tools = make_mcp_tools(config) # discovers the allowlisted tools once (tools/list)
|
|
34
|
+
|
|
35
|
+
openai_tools = [t.to_openai_schema() for t in tools] # or t.to_gemini_schema()
|
|
36
|
+
result = tools[0].call(repoName="sqlite/sqlite")
|
|
37
|
+
print(result.success, result.result) # True {"text": "..."}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Nothing is read from the environment: your application builds the config.
|
|
41
|
+
Tools are exposed as `<server>__<tool>` (characters outside letters, digits,
|
|
42
|
+
`_` and `-` become `_`, at most 64 characters).
|
|
43
|
+
|
|
44
|
+
To keep server-controlled text out of the model's context entirely, give the
|
|
45
|
+
descriptions and schemas yourself; nothing is sent over the network at setup:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from ragleap_integrations import McpToolSpec
|
|
49
|
+
|
|
50
|
+
spec = McpToolSpec(
|
|
51
|
+
server="deepwiki", name="read_wiki_structure",
|
|
52
|
+
description="List the documentation topics of a GitHub repository.",
|
|
53
|
+
parameters={"type": "object", "properties": {"repoName": {"type": "string"}}, "required": ["repoName"]},
|
|
54
|
+
)
|
|
55
|
+
tools = make_mcp_tools(config, specs=[spec])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Trust model
|
|
59
|
+
|
|
60
|
+
- Everything a server says is untrusted text: tool descriptions, input
|
|
61
|
+
schemas and results flow into a model's context. This package does **not**
|
|
62
|
+
screen them for prompt injection. The structural limits are the allowlist,
|
|
63
|
+
a snapshot of the tool list taken once at setup (a server that changes a
|
|
64
|
+
description later has no effect), length caps, and optional owner-supplied
|
|
65
|
+
specs.
|
|
66
|
+
- Server-supplied error text is never put in a result; failures carry a
|
|
67
|
+
constant message plus, at most, an HTTP status or a JSON-RPC error code.
|
|
68
|
+
- Network access is bounded in code: https only, no credentials in the URL,
|
|
69
|
+
no IP-literal hosts, DNS resolved once with every address required to be
|
|
70
|
+
public (so DNS rebinding cannot swap in an internal address), the
|
|
71
|
+
connection pinned to that IP with TLS (minimum 1.2) verified against the
|
|
72
|
+
hostname, no redirects, no compressed responses, a response-size cap, and a
|
|
73
|
+
wall-clock deadline that also covers the TLS handshake and the headers.
|
|
74
|
+
- A bearer token, if configured, is sent only to its own server URL and is
|
|
75
|
+
never logged.
|
|
76
|
+
|
|
77
|
+
## Protocol
|
|
78
|
+
|
|
79
|
+
The current revision (2026-07-28) is tried first: every request is its own
|
|
80
|
+
POST carrying `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers and
|
|
81
|
+
the version, client info and capabilities in `params._meta`; parameters marked
|
|
82
|
+
`x-mcp-header` in a tool's schema are mirrored into `Mcp-Param-*` headers.
|
|
83
|
+
A 4xx without a recognized modern error (`-32020`, `-32021`, `-32022`)
|
|
84
|
+
identifies a legacy server, and the client then uses the legacy `initialize`
|
|
85
|
+
handshake with protocol version 2025-06-18 only (an explicit allowlist, not
|
|
86
|
+
"whatever the server answers"). The era found is remembered per server.
|
|
87
|
+
|
|
88
|
+
## Limits and defaults
|
|
89
|
+
|
|
90
|
+
| Setting | Default |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Whole-call deadline (`total_timeout`) | 30 s |
|
|
93
|
+
| Per-operation socket timeout (`op_timeout`) | 10 s |
|
|
94
|
+
| Response size (`max_response_bytes`) | 256 KiB |
|
|
95
|
+
| Result text (`max_result_chars`) | 4000 characters |
|
|
96
|
+
| Tool description (`max_description_chars`) | 500 characters |
|
|
97
|
+
| Input schema (`max_schema_chars`) | 10000 characters (larger: tool skipped) |
|
|
98
|
+
|
|
99
|
+
## Known limitations
|
|
100
|
+
|
|
101
|
+
- On a legacy server every tool call performs a full `initialize` handshake
|
|
102
|
+
(three POSTs) and never closes the session.
|
|
103
|
+
- A `HeaderMismatch` error is reported, not retried after re-reading the
|
|
104
|
+
tool list.
|
|
105
|
+
- Only text content is returned; other content types are counted and omitted.
|
|
106
|
+
- `ragleap_tools.Tool.call` in ragleap-tools 0.4.0 cannot take an argument
|
|
107
|
+
literally named `self`.
|
|
108
|
+
- Not safe to share one client between threads while calls are running.
|
|
109
|
+
|
|
110
|
+
## Verification status
|
|
111
|
+
|
|
112
|
+
- 201 tests, including a real local TLS server with a throwaway certificate
|
|
113
|
+
for the transport (hostname verification, IP pinning, size cap, truncated
|
|
114
|
+
bodies, and the deadline against slow-drip bodies, slow-drip headers and a
|
|
115
|
+
stalled handshake).
|
|
116
|
+
- **Live-checked on 2026-10-05** against DeepWiki's public MCP server
|
|
117
|
+
(`https://mcp.deepwiki.com/mcp`, no authentication): the server answered
|
|
118
|
+
the modern request with HTTP 400 and a JSON-RPC `-32600` error, the client
|
|
119
|
+
fell back to the legacy handshake, negotiated 2025-06-18, listed tools and
|
|
120
|
+
read a repository's documentation structure. This verifies the legacy path,
|
|
121
|
+
the fallback decision, event-stream responses and the pinned TLS transport
|
|
122
|
+
against a real server.
|
|
123
|
+
- **Not live-verified:** the modern 2026-07-28 path, `x-mcp-header`
|
|
124
|
+
mirroring, where a `-32022` error carries its supported-version list,
|
|
125
|
+
bearer-token authentication, plain JSON (non-stream) responses and
|
|
126
|
+
pagination. They follow the public specification and are covered by tests
|
|
127
|
+
against fakes only; treat them as best-effort until confirmed live.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# MCP client - allowlisted MCP tools as ragleap-tools Tools
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
core/mcp_client.py (in the app) already calls allowlisted tools on
|
|
6
|
+
remote MCP servers, but it is coupled to the app: configuration is read
|
|
7
|
+
from environment variables, it depends on requests and
|
|
8
|
+
core.action_senders, and it makes three POSTs per call (initialize,
|
|
9
|
+
notifications/initialized, tools/call). It never calls tools/list, so it
|
|
10
|
+
has no tool schema (the caller passes arguments as a JSON string), it
|
|
11
|
+
keeps text content only, and it pins protocol revision 2025-06-18. Its
|
|
12
|
+
tests use an in-process fake server, not a real one.
|
|
13
|
+
|
|
14
|
+
This package extracts and hardens that client and returns
|
|
15
|
+
ragleap_tools.Tool objects, so MCP tools and native tools share one shape.
|
|
16
|
+
|
|
17
|
+
## Scope (v0.1.0)
|
|
18
|
+
|
|
19
|
+
MCP client over Streamable HTTP only. Deliberately excluded:
|
|
20
|
+
- stdio transport: it launches a local subprocess, i.e. code execution.
|
|
21
|
+
- HTTP+SSE: deprecated since 2025-03-26.
|
|
22
|
+
- Sampling, elicitation and roots (InputRequiredResult): v0.1.0 returns a
|
|
23
|
+
clear "unsupported" ToolResult error.
|
|
24
|
+
- Resources, prompts, subscriptions/listen, OAuth.
|
|
25
|
+
- Code execution, HTTP fetch, database/business-system connectors and the
|
|
26
|
+
agent loop: each is a separate security design pass or belongs to
|
|
27
|
+
ragleap-agents.
|
|
28
|
+
|
|
29
|
+
## Design
|
|
30
|
+
|
|
31
|
+
Explicit configuration, no environment-variable fallback (the same BYOK
|
|
32
|
+
rule as ragleap-tools); the app builds the config from its env vars:
|
|
33
|
+
- McpServerConfig(name, url, token=None): static bearer token only.
|
|
34
|
+
- McpConfig(servers, allowed_tools): exact "server.tool" allowlist. The
|
|
35
|
+
model supplies only arguments; it never chooses a URL, command or token.
|
|
36
|
+
|
|
37
|
+
make_mcp_tools(config) returns one ragleap_tools.Tool per allowlisted
|
|
38
|
+
tool, two ways to get the schema:
|
|
39
|
+
1. Owner-supplied specs (no network, safest): the owner states each
|
|
40
|
+
tool's description and parameters.
|
|
41
|
+
2. Discovery: tools/list, restricted to allowlisted names, snapshotted
|
|
42
|
+
once and not refreshed per call. A server that changes a description
|
|
43
|
+
later has no effect. Descriptions are length-capped. Tool definitions
|
|
44
|
+
with invalid x-mcp-header annotations are excluded with a warning, as
|
|
45
|
+
the spec requires.
|
|
46
|
+
|
|
47
|
+
## Protocol
|
|
48
|
+
|
|
49
|
+
Two code paths, chosen per the spec's Backward Compatibility section:
|
|
50
|
+
- Modern (2026-07-28): each request is its own POST with Accept (JSON and
|
|
51
|
+
event-stream), MCP-Protocol-Version, Mcp-Method, and Mcp-Name for
|
|
52
|
+
tools/call, plus protocolVersion, clientInfo and clientCapabilities in
|
|
53
|
+
_meta. No initialize, no session. x-mcp-header parameters are mirrored
|
|
54
|
+
into Mcp-Param-* headers. Both application/json and text/event-stream
|
|
55
|
+
responses are handled.
|
|
56
|
+
- Legacy (2025-06-18): initialize, notifications/initialized, then
|
|
57
|
+
tools/call, with Mcp-Session-Id when the server issues one. Ported from
|
|
58
|
+
core/mcp_client.py.
|
|
59
|
+
Try modern first, declaring the newest modern revision this package
|
|
60
|
+
implements. A modern server that does not support it answers with
|
|
61
|
+
UnsupportedProtocolVersionError listing the versions it does support (the
|
|
62
|
+
spec's example is 2026-07-28 and 2025-11-25); retry once with a mutually
|
|
63
|
+
supported version, otherwise report an error. server/discover is optional
|
|
64
|
+
and is not used in v0.1.0. A 4xx without a recognized modern error body
|
|
65
|
+
means a legacy server: fall back to legacy. The recognized modern errors are
|
|
66
|
+
-32020 (HeaderMismatch), -32021 (MissingRequiredClientCapability) and -32022
|
|
67
|
+
(UnsupportedProtocolVersion); on a 400 they are reported, never treated as
|
|
68
|
+
legacy (a -32022 whose supported list includes an allowlisted legacy version
|
|
69
|
+
continues with the legacy handshake). Any other JSON-RPC error on a 400 - a
|
|
70
|
+
live DeepWiki server answered -32600 - is a legacy server's own rejection and
|
|
71
|
+
falls back too. A fallback that then fails reports the legacy failure together
|
|
72
|
+
with the first reply's code, so a modern server that answered 400 and -32602
|
|
73
|
+
for bad arguments is not hidden. (The code numbers come from an SDK issue
|
|
74
|
+
summarising the spec's rule, a secondary source; the principle is in the spec.)
|
|
75
|
+
|
|
76
|
+
Legacy revisions are an explicit allowlist, not "whatever the server
|
|
77
|
+
answers": the app client accepts the version the server returns without
|
|
78
|
+
checking it, and this package will not. Which legacy revisions are
|
|
79
|
+
supported (2025-06-18 as in the app client, 2025-11-25, or both) is
|
|
80
|
+
decided after reading those revisions' transport pages.
|
|
81
|
+
|
|
82
|
+
## Network safety
|
|
83
|
+
|
|
84
|
+
https only, public addresses only (IPv4-mapped IPv6 addresses unwrapped
|
|
85
|
+
before the check); resolve DNS once and connect to that IP, then wrap the
|
|
86
|
+
socket in TLS (minimum 1.2) verified against the hostname and hand it to
|
|
87
|
+
http.client, the technique used in core/page_fetch.py; no redirects;
|
|
88
|
+
Accept-Encoding: identity; a response-size cap; constant error messages
|
|
89
|
+
(no library exception text in results); the token is sent only to its
|
|
90
|
+
configured URL and never logged.
|
|
91
|
+
|
|
92
|
+
Wall-clock deadline. core/page_fetch.py checks its deadline only between
|
|
93
|
+
chunks. A test on 2026-10-03 (plain HTTP on loopback) showed that a server
|
|
94
|
+
sending one byte every 0.2 s kept resp.read(8192) blocked for 80 s against
|
|
95
|
+
a 3 s deadline, because every byte arrived inside the per-operation
|
|
96
|
+
timeout. Two fixes were tried: read1() bounds the body phase (stopped at
|
|
97
|
+
3.0 s); a watchdog timer that shuts the socket down at the deadline bounds
|
|
98
|
+
both the header and the body phase (3.0 s each) but the read then looks
|
|
99
|
+
like a normal end of response, so the watchdog must set a flag and the
|
|
100
|
+
caller must raise a deadline error when it is set. This package uses the
|
|
101
|
+
watchdog plus read1(), started before the TLS handshake. Not yet tested
|
|
102
|
+
over TLS; that is a test with a local TLS server.
|
|
103
|
+
|
|
104
|
+
## Dependencies
|
|
105
|
+
|
|
106
|
+
ragleap-tools (for Tool and ToolResult) and otherwise the standard library
|
|
107
|
+
(http.client, ssl, json). No requests.
|
|
108
|
+
|
|
109
|
+
## Results and errors
|
|
110
|
+
|
|
111
|
+
Text content parts only; other content types are dropped and the result
|
|
112
|
+
says so. isError maps to ToolResult(success=False). Result length is
|
|
113
|
+
capped. Every failure (allowlist refusal, bad URL, network error, non-2xx,
|
|
114
|
+
malformed JSON-RPC, InputRequiredResult) is a ToolResult, never an
|
|
115
|
+
exception.
|
|
116
|
+
|
|
117
|
+
## Untrusted content
|
|
118
|
+
|
|
119
|
+
Tool descriptions, input schemas and results are text controlled by the
|
|
120
|
+
MCP server and flow into the model's context. This is a prompt-injection
|
|
121
|
+
surface and is not screened. Mitigations here are structural (the
|
|
122
|
+
allowlist, the snapshot, description caps, optional owner-supplied
|
|
123
|
+
specs), not content filtering.
|
|
124
|
+
|
|
125
|
+
## Testing
|
|
126
|
+
|
|
127
|
+
- One injectable transport function carries all HTTP, so tests exercise the
|
|
128
|
+
real header and body construction without a network (the ragleap-tools
|
|
129
|
+
convention of mocking at the lowest seam). The address check takes an
|
|
130
|
+
injectable resolver.
|
|
131
|
+
- The address check gets its own tests, because the app's tests replace it
|
|
132
|
+
with True: private, loopback, link-local including the cloud metadata
|
|
133
|
+
address, multicast, IPv4-mapped IPv6, and a host with one public and one
|
|
134
|
+
private address (must be refused).
|
|
135
|
+
- Ported behavior: allowlist parsing and refusal, no redirects, timeouts,
|
|
136
|
+
size cap, session handling on the legacy path, isError, text-only content.
|
|
137
|
+
- Deadline: a slow-drip server (one byte per interval, under the
|
|
138
|
+
per-operation timeout, in both the header and the body phase) must
|
|
139
|
+
produce a deadline error, never a truncated success.
|
|
140
|
+
- Modern path: MCP-Protocol-Version, Mcp-Method and Mcp-Name headers,
|
|
141
|
+
_meta contents, x-mcp-header mirroring including the Base64 sentinel
|
|
142
|
+
encoding, rejection of invalid annotations, UnsupportedProtocolVersionError
|
|
143
|
+
retry, and the 400-without-modern-error fallback.
|
|
144
|
+
|
|
145
|
+
## Packaging and CI
|
|
146
|
+
|
|
147
|
+
- packages/ragleap-integrations, hatchling build, src/ragleap_integrations,
|
|
148
|
+
dependencies = ["ragleap-tools>=0.4.0"], the same layout as ragleap-tools.
|
|
149
|
+
- A ragleap-integrations-tests CI job on a Python 3.10/3.11/3.12 matrix
|
|
150
|
+
(ragleap-vectorstores already runs 3.10 and 3.12). ragleap-tools' own CI
|
|
151
|
+
runs 3.11 only although its classifiers list 3.10 to 3.12: a small
|
|
152
|
+
separate follow-up.
|
|
153
|
+
- release.yml gets one new job, one dispatch option and one tag pattern.
|
|
154
|
+
It is shared with other packages' jobs; nothing else in it changes.
|
|
155
|
+
- First-release prerequisite: the PyPI and TestPyPI projects do not exist
|
|
156
|
+
yet (the name was free on 2026-10-03). The account owner must add a
|
|
157
|
+
pending trusted publisher on each (repository antonyrag/ragleap-core,
|
|
158
|
+
workflow release.yml, environment pypi or testpypi).
|
|
159
|
+
|
|
160
|
+
## Verification status
|
|
161
|
+
|
|
162
|
+
Checked against the current public specification (the 2026-07-28
|
|
163
|
+
Streamable HTTP page, changelog and versioning page): per-request headers
|
|
164
|
+
and _meta, no sessions, no negotiation handshake, UnsupportedProtocolVersionError
|
|
165
|
+
with a supported list, optional server/discover, x-mcp-header mirroring,
|
|
166
|
+
backward-compatibility detection, and InputRequiredResult for
|
|
167
|
+
server-to-client interactions. Not yet checked: the tools page (tools/list
|
|
168
|
+
and inputSchema field names, pagination), the InputRequiredResult shape,
|
|
169
|
+
and the 2025-06-18 and 2025-11-25 transport pages.
|
|
170
|
+
Live check (2026-10-05): the package's real transport and client were run against
|
|
171
|
+
DeepWiki's public MCP server (https://mcp.deepwiki.com/mcp, no authentication)
|
|
172
|
+
with every exchange traced. The server answered the modern request with HTTP
|
|
173
|
+
400 and JSON-RPC -32600 (an unsupported-protocol-version message), which
|
|
174
|
+
exposed a bug in the first version of the detection rule (any JSON-RPC error
|
|
175
|
+
was treated as a modern server). After the fix the client fell back to the
|
|
176
|
+
legacy handshake, the server agreed to 2025-06-18, notifications/initialized
|
|
177
|
+
returned 202, tools/list and tools/call succeeded over event-stream responses
|
|
178
|
+
(CRLF framing), and read_wiki_structure returned a repository's documentation
|
|
179
|
+
structure. Verified live: the fallback decision, the legacy handshake,
|
|
180
|
+
event-stream parsing, discovery and the pinned TLS transport. Not verified
|
|
181
|
+
live: the modern 2026-07-28 path, x-mcp-header mirroring, where a -32022 error
|
|
182
|
+
carries its supported-version list, bearer-token authentication, plain JSON
|
|
183
|
+
responses and pagination. The existing app client had only ever run against a
|
|
184
|
+
fake in-process server.
|
|
185
|
+
|
|
186
|
+
## Open decisions (resolved when v0.1.0 was built)
|
|
187
|
+
|
|
188
|
+
Resolved: (1) both paths, with legacy limited to 2025-06-18; (2) DeepWiki's
|
|
189
|
+
public server was used for a live check, which exercises the legacy path only;
|
|
190
|
+
(3) not part of v0.1.0; (4) tools are exposed as <server>__<tool>, sanitized
|
|
191
|
+
and cut to 64 characters, and a collision is a setup error; (5) an explicit
|
|
192
|
+
port in a configured URL is allowed. The list below is kept as drafted.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
1. Modern plus legacy, or modern only.
|
|
196
|
+
2. A public HTTPS MCP server for a live check; otherwise the package ships
|
|
197
|
+
labelled unverified.
|
|
198
|
+
3. Whether core/ later imports this package instead of its own client
|
|
199
|
+
(not part of v0.1.0).
|
|
200
|
+
4. Tool naming: function-calling APIs restrict name characters, so the
|
|
201
|
+
"server.tool" allowlist key is not the exposed Tool name.
|
|
202
|
+
5. Ports: core/page_fetch.py allows 443 only because the model chooses its
|
|
203
|
+
URL. Here the owner chooses the URL, so an explicit port in a configured
|
|
204
|
+
URL could be allowed (still pinned and public-only). Proposed default:
|
|
205
|
+
allow it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ragleap-integrations"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP client that turns allowlisted tools on remote MCP servers into ragleap-tools Tool objects. Streamable HTTP only, standard library plus ragleap-tools, owner-configured allowlist, pinned and bounded network access. BYOK, no vendor lock-in."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Antony", email = "antony@ragleap.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "model-context-protocol", "llm", "tool-calling", "agents", "integrations", "self-hosted", "byok"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = ["ragleap-tools>=0.4.0"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=8.0.0"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/antonyrag/ragleap-core"
|
|
34
|
+
Repository = "https://github.com/antonyrag/ragleap-core"
|
|
35
|
+
Documentation = "https://github.com/antonyrag/ragleap-core/tree/main/packages/ragleap-integrations"
|
|
36
|
+
Issues = "https://github.com/antonyrag/ragleap-core/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/ragleap_integrations"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ragleap_integrations - connectors that return ragleap_tools.Tool objects.
|
|
3
|
+
|
|
4
|
+
v0.1.0: an MCP client over Streamable HTTP (see docs/design/mcp-client.md).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from ragleap_integrations.mcp import (
|
|
10
|
+
Discovery,
|
|
11
|
+
McpClient,
|
|
12
|
+
McpConfig,
|
|
13
|
+
McpConfigError,
|
|
14
|
+
McpDiscoveryError,
|
|
15
|
+
McpServerConfig,
|
|
16
|
+
McpToolSpec,
|
|
17
|
+
make_mcp_tools,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"Discovery",
|
|
23
|
+
"McpClient",
|
|
24
|
+
"McpConfig",
|
|
25
|
+
"McpConfigError",
|
|
26
|
+
"McpDiscoveryError",
|
|
27
|
+
"McpServerConfig",
|
|
28
|
+
"McpToolSpec",
|
|
29
|
+
"make_mcp_tools",
|
|
30
|
+
]
|