onetool-mcp 1.0.0b1__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.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
ot_tools/context7.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""Context7 API tools for library search and documentation.
|
|
2
|
+
|
|
3
|
+
These built-in tools provide access to the Context7 documentation API
|
|
4
|
+
for fetching up-to-date library documentation and code examples.
|
|
5
|
+
|
|
6
|
+
Based on context7 by Upstash (MIT License).
|
|
7
|
+
https://github.com/upstash/context7
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Pack for dot notation: context7.search(), context7.doc()
|
|
13
|
+
pack = "context7"
|
|
14
|
+
|
|
15
|
+
__all__ = ["doc", "search"]
|
|
16
|
+
|
|
17
|
+
# Dependency declarations for CLI validation
|
|
18
|
+
__ot_requires__ = {
|
|
19
|
+
"secrets": ["CONTEXT7_API_KEY"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
|
|
27
|
+
from ot.config import get_secret, get_tool_config
|
|
28
|
+
from ot.logging import LogSpan
|
|
29
|
+
from ot.utils import cache
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Config(BaseModel):
|
|
33
|
+
"""Pack configuration - discovered by registry."""
|
|
34
|
+
|
|
35
|
+
timeout: float = Field(
|
|
36
|
+
default=30.0,
|
|
37
|
+
ge=1.0,
|
|
38
|
+
le=120.0,
|
|
39
|
+
description="Request timeout in seconds",
|
|
40
|
+
)
|
|
41
|
+
docs_limit: int = Field(
|
|
42
|
+
default=10,
|
|
43
|
+
ge=1,
|
|
44
|
+
le=20,
|
|
45
|
+
description="Maximum number of documentation items to return",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Context7 REST API configuration
|
|
49
|
+
CONTEXT7_SEARCH_URL = "https://context7.com/api/v2/search"
|
|
50
|
+
CONTEXT7_DOCS_CODE_URL = "https://context7.com/api/v2/docs/code"
|
|
51
|
+
CONTEXT7_DOCS_INFO_URL = "https://context7.com/api/v2/docs/info"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_config() -> Config:
|
|
55
|
+
"""Get context7 pack configuration."""
|
|
56
|
+
return get_tool_config("context7", Config)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Shared HTTP client for connection pooling
|
|
60
|
+
_client = httpx.Client(timeout=30.0)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_api_key() -> str:
|
|
64
|
+
"""Get Context7 API key from secrets."""
|
|
65
|
+
return get_secret("CONTEXT7_API_KEY") or ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_headers() -> dict[str, str]:
|
|
69
|
+
"""Get authorization headers for Context7 API."""
|
|
70
|
+
api_key = _get_api_key()
|
|
71
|
+
if api_key:
|
|
72
|
+
return {"Authorization": f"Bearer {api_key}"}
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_request(
|
|
77
|
+
url: str,
|
|
78
|
+
params: dict[str, str | int] | None = None,
|
|
79
|
+
timeout: float | None = None,
|
|
80
|
+
) -> tuple[bool, str | dict]:
|
|
81
|
+
"""Make HTTP GET request to Context7 API.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
url: Full URL to request
|
|
85
|
+
params: Query parameters
|
|
86
|
+
timeout: Request timeout in seconds (defaults to config)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple of (success, result). If success, result is parsed JSON or text.
|
|
90
|
+
If failure, result is error message string.
|
|
91
|
+
"""
|
|
92
|
+
api_key = _get_api_key()
|
|
93
|
+
if not api_key:
|
|
94
|
+
return False, "[Context7 API key not configured]"
|
|
95
|
+
|
|
96
|
+
if timeout is None:
|
|
97
|
+
timeout = _get_config().timeout
|
|
98
|
+
|
|
99
|
+
with LogSpan(span="context7.request", url=url) as span:
|
|
100
|
+
try:
|
|
101
|
+
response = _client.get(
|
|
102
|
+
url,
|
|
103
|
+
params=params,
|
|
104
|
+
headers=_get_headers(),
|
|
105
|
+
timeout=timeout,
|
|
106
|
+
)
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
|
|
109
|
+
content_type = response.headers.get("content-type", "")
|
|
110
|
+
span.add(status=response.status_code)
|
|
111
|
+
if "application/json" in content_type:
|
|
112
|
+
return True, response.json()
|
|
113
|
+
return True, response.text
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
error_type = type(e).__name__
|
|
117
|
+
span.add(error=f"{error_type}: {e}")
|
|
118
|
+
if hasattr(e, "response"):
|
|
119
|
+
status = getattr(e.response, "status_code", "unknown")
|
|
120
|
+
return False, f"HTTP error ({status}): {error_type}"
|
|
121
|
+
return False, f"Request failed: {e}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@cache(ttl=3600) # Cache library key resolutions for 1 hour
|
|
125
|
+
def _normalize_library_key(library_key: str) -> str:
|
|
126
|
+
"""Normalize library key to Context7 API format.
|
|
127
|
+
|
|
128
|
+
Handles various input formats and common issues:
|
|
129
|
+
- "/vercel/next.js/v16.0.3" -> "vercel/next.js"
|
|
130
|
+
- "/vercel/next.js" -> "vercel/next.js"
|
|
131
|
+
- "vercel/next.js" -> "vercel/next.js"
|
|
132
|
+
- "next.js" -> "next.js" (search will be needed)
|
|
133
|
+
- "https://github.com/vercel/next.js" -> "vercel/next.js"
|
|
134
|
+
- Stray quotes: '"vercel/next.js"' -> "vercel/next.js"
|
|
135
|
+
- Double slashes: "vercel//next.js" -> "vercel/next.js"
|
|
136
|
+
- Trailing slashes: "vercel/next.js/" -> "vercel/next.js"
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
library_key: Raw library key from user input
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Normalized org/repo format for Context7 API
|
|
143
|
+
"""
|
|
144
|
+
key = library_key.strip()
|
|
145
|
+
|
|
146
|
+
# Remove stray quotes (single or double)
|
|
147
|
+
key = key.strip("\"'")
|
|
148
|
+
|
|
149
|
+
# Handle GitHub URLs
|
|
150
|
+
github_match = re.match(r"https?://(?:www\.)?github\.com/([^/]+)/([^/]+)/?.*", key)
|
|
151
|
+
if github_match:
|
|
152
|
+
return f"{github_match.group(1)}/{github_match.group(2)}"
|
|
153
|
+
|
|
154
|
+
# Handle Context7 URLs
|
|
155
|
+
context7_match = re.match(
|
|
156
|
+
r"https?://(?:www\.)?context7\.com/([^/]+)/([^/]+)/?.*", key
|
|
157
|
+
)
|
|
158
|
+
if context7_match:
|
|
159
|
+
return f"{context7_match.group(1)}/{context7_match.group(2)}"
|
|
160
|
+
|
|
161
|
+
# Fix double slashes
|
|
162
|
+
while "//" in key:
|
|
163
|
+
key = key.replace("//", "/")
|
|
164
|
+
|
|
165
|
+
# Strip leading and trailing slashes
|
|
166
|
+
key = key.strip("/")
|
|
167
|
+
|
|
168
|
+
# Extract org/repo (ignore version suffix like /v16.0.3)
|
|
169
|
+
parts = key.split("/")
|
|
170
|
+
if len(parts) >= 2:
|
|
171
|
+
# Check if third part looks like a version
|
|
172
|
+
if len(parts) > 2 and re.match(r"v?\d+", parts[2]):
|
|
173
|
+
return f"{parts[0]}/{parts[1]}"
|
|
174
|
+
# Otherwise just take first two parts
|
|
175
|
+
return f"{parts[0]}/{parts[1]}"
|
|
176
|
+
|
|
177
|
+
return key
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _normalize_topic(topic: str) -> str:
|
|
181
|
+
"""Normalize topic string for search.
|
|
182
|
+
|
|
183
|
+
Handles:
|
|
184
|
+
- Stray quotes: '"PPR"' -> "PPR"
|
|
185
|
+
- Placeholder syntax: "<relevant topic>" -> ""
|
|
186
|
+
- Path-like topics: "app/partial-pre-rendering/index" -> "partial pre-rendering"
|
|
187
|
+
- Extra whitespace
|
|
188
|
+
- Escaped quotes: '\\"topic\\"' -> "topic"
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
topic: Raw topic from user input
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Cleaned topic string
|
|
195
|
+
"""
|
|
196
|
+
topic = topic.strip()
|
|
197
|
+
|
|
198
|
+
# Remove stray quotes (single or double)
|
|
199
|
+
topic = topic.strip("\"'")
|
|
200
|
+
|
|
201
|
+
# Remove escaped quotes
|
|
202
|
+
topic = topic.replace('\\"', "").replace("\\'", "")
|
|
203
|
+
|
|
204
|
+
# Remove placeholder markers
|
|
205
|
+
if topic.startswith("<") and topic.endswith(">"):
|
|
206
|
+
topic = topic[1:-1].strip()
|
|
207
|
+
|
|
208
|
+
# If it's a placeholder like "relevant topic", return empty to get general docs
|
|
209
|
+
if topic.lower() in ("relevant topic", "topic", "extract from question", ""):
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
# Convert path-like topics to search terms
|
|
213
|
+
# "app/partial-pre-rendering/index" -> "partial pre-rendering"
|
|
214
|
+
if "/" in topic and not topic.startswith("http"):
|
|
215
|
+
# Take the most specific part (usually the last meaningful segment)
|
|
216
|
+
parts = [p for p in topic.split("/") if p and p != "index"]
|
|
217
|
+
if parts:
|
|
218
|
+
topic = parts[-1]
|
|
219
|
+
|
|
220
|
+
# Convert kebab-case to spaces
|
|
221
|
+
topic = topic.replace("-", " ").replace("_", " ")
|
|
222
|
+
|
|
223
|
+
# Clean up whitespace
|
|
224
|
+
topic = " ".join(topic.split())
|
|
225
|
+
|
|
226
|
+
return topic
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def search(*, query: str, output_format: str = "str") -> str | dict | list:
|
|
230
|
+
"""Search for libraries by name in Context7.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
query: The search query (e.g., 'next.js', 'react', 'vue')
|
|
234
|
+
output_format: Response format - 'str' for string (default), 'dict' for raw dict/list
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Search results with matching libraries and their IDs.
|
|
238
|
+
If output_format='dict', returns the raw API response (dict or list).
|
|
239
|
+
If output_format='str', returns a string representation.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
context7.search(query="fastapi")
|
|
243
|
+
context7.search(query="react hooks")
|
|
244
|
+
context7.search(query="flask", output_format="dict") # Returns raw dict
|
|
245
|
+
"""
|
|
246
|
+
if output_format not in ("str", "dict"):
|
|
247
|
+
return f"Invalid output_format '{output_format}'. Valid options: 'str', 'dict'."
|
|
248
|
+
|
|
249
|
+
with LogSpan(span="context7.search", query=query, output_format=output_format) as s:
|
|
250
|
+
success, result = _make_request(CONTEXT7_SEARCH_URL, params={"query": query})
|
|
251
|
+
|
|
252
|
+
s.add(success=success)
|
|
253
|
+
if not success:
|
|
254
|
+
return f"{result} query={query}"
|
|
255
|
+
|
|
256
|
+
if output_format == "dict":
|
|
257
|
+
s.add(resultType=type(result).__name__)
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
result_str = str(result)
|
|
261
|
+
s.add(resultLen=len(result_str))
|
|
262
|
+
return result_str
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _pick_best_library(data: dict | list | None, query: str) -> str | None:
|
|
266
|
+
"""Pick the best library from Context7 search response.
|
|
267
|
+
|
|
268
|
+
Handles {"results": [...]} and [...] response formats.
|
|
269
|
+
|
|
270
|
+
Scoring:
|
|
271
|
+
- Exact title match: +100
|
|
272
|
+
- Title contains query: +50
|
|
273
|
+
- VIP library: +30
|
|
274
|
+
- Verified: +20
|
|
275
|
+
- Trust score: +trustScore * 2
|
|
276
|
+
- Large corpus (>100k tokens): +5
|
|
277
|
+
"""
|
|
278
|
+
# Unwrap response
|
|
279
|
+
if isinstance(data, dict):
|
|
280
|
+
results = data.get("results", [])
|
|
281
|
+
elif isinstance(data, list):
|
|
282
|
+
results = data
|
|
283
|
+
else:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
if not results:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
query_lower = query.lower()
|
|
290
|
+
|
|
291
|
+
def score_result(r: dict) -> float:
|
|
292
|
+
score = 0.0
|
|
293
|
+
title = r.get("title", "").lower()
|
|
294
|
+
|
|
295
|
+
if title == query_lower:
|
|
296
|
+
score += 100
|
|
297
|
+
elif query_lower in title:
|
|
298
|
+
score += 50
|
|
299
|
+
|
|
300
|
+
if r.get("vip"):
|
|
301
|
+
score += 30
|
|
302
|
+
if r.get("verified"):
|
|
303
|
+
score += 20
|
|
304
|
+
|
|
305
|
+
trust = r.get("trustScore", 0)
|
|
306
|
+
if trust > 0:
|
|
307
|
+
score += trust * 2
|
|
308
|
+
|
|
309
|
+
if r.get("totalTokens", 0) > 100000:
|
|
310
|
+
score += 5
|
|
311
|
+
|
|
312
|
+
# Star-based scoring (capped at 20 bonus points)
|
|
313
|
+
stars = r.get("stars", 0)
|
|
314
|
+
if stars > 0:
|
|
315
|
+
score += min(stars / 1000, 20)
|
|
316
|
+
|
|
317
|
+
# Benchmark score contribution (max ~10 bonus points)
|
|
318
|
+
benchmark = r.get("benchmarkScore", 0)
|
|
319
|
+
if benchmark > 0:
|
|
320
|
+
score += benchmark / 10
|
|
321
|
+
|
|
322
|
+
return score
|
|
323
|
+
|
|
324
|
+
sorted_results = sorted(results, key=score_result, reverse=True)
|
|
325
|
+
best = sorted_results[0]
|
|
326
|
+
lib_id = best.get("id", "").lstrip("/")
|
|
327
|
+
|
|
328
|
+
return lib_id if "/" in lib_id else None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@cache(ttl=3600) # Cache library key resolutions for 1 hour
|
|
332
|
+
def _resolve_library_key(library_key: str) -> tuple[str, bool, bool]:
|
|
333
|
+
"""Resolve a library key, searching if needed.
|
|
334
|
+
|
|
335
|
+
If the key doesn't look like a valid org/repo format,
|
|
336
|
+
search Context7 to find the best match.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
library_key: Raw or partial library key
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Tuple of (resolved org/repo library key, was_searched, found_match).
|
|
343
|
+
was_searched is True if a search was performed to resolve the key.
|
|
344
|
+
found_match is True if search found a valid library match.
|
|
345
|
+
"""
|
|
346
|
+
normalized = _normalize_library_key(library_key)
|
|
347
|
+
|
|
348
|
+
# If it looks like a valid org/repo, use it directly
|
|
349
|
+
if "/" in normalized and len(normalized.split("/")) == 2:
|
|
350
|
+
org, repo = normalized.split("/")
|
|
351
|
+
if org and repo and not org.startswith("http"):
|
|
352
|
+
return normalized, False, True
|
|
353
|
+
|
|
354
|
+
# Otherwise, search for the library
|
|
355
|
+
success, data = _make_request(CONTEXT7_SEARCH_URL, params={"query": normalized})
|
|
356
|
+
|
|
357
|
+
if not success:
|
|
358
|
+
return normalized, True, False
|
|
359
|
+
|
|
360
|
+
# Use smart scoring to pick the best match
|
|
361
|
+
best = _pick_best_library(data, normalized)
|
|
362
|
+
if best:
|
|
363
|
+
return best, True, True
|
|
364
|
+
return normalized, True, False
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def doc(
|
|
368
|
+
*,
|
|
369
|
+
library_key: str,
|
|
370
|
+
topic: str = "",
|
|
371
|
+
mode: str = "info",
|
|
372
|
+
page: int = 1,
|
|
373
|
+
limit: int | None = None,
|
|
374
|
+
doc_type: str = "txt",
|
|
375
|
+
version: str | None = None,
|
|
376
|
+
) -> str:
|
|
377
|
+
"""Fetch documentation for a library from Context7.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
library_key: The library key - can be flexible format:
|
|
381
|
+
- Full: 'vercel/next.js'
|
|
382
|
+
- With version: '/vercel/next.js/v16.0.3'
|
|
383
|
+
- Shorthand: 'next.js', 'nextjs', 'react'
|
|
384
|
+
- URL: 'https://github.com/vercel/next.js'
|
|
385
|
+
topic: Topic to focus documentation on (e.g., 'routing', 'hooks', 'ssr').
|
|
386
|
+
Default: empty string for general docs
|
|
387
|
+
mode: Documentation mode - 'info' for conceptual guides and narrative documentation (default),
|
|
388
|
+
'code' for API references and code examples
|
|
389
|
+
page: Page number for pagination (default: 1, max: 10)
|
|
390
|
+
limit: Number of results per page (defaults to config, max: config docs_limit)
|
|
391
|
+
doc_type: Response format 'txt' or 'json' (default: 'txt')
|
|
392
|
+
version: Optional version suffix (e.g., 'v16.0.3'). If provided, appended to library key.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Documentation content and code examples for the requested topic
|
|
396
|
+
|
|
397
|
+
Example:
|
|
398
|
+
# Get general docs
|
|
399
|
+
context7.doc(library_key="fastapi/fastapi")
|
|
400
|
+
|
|
401
|
+
# Get docs on a specific topic
|
|
402
|
+
context7.doc(library_key="vercel/next.js", topic="routing")
|
|
403
|
+
|
|
404
|
+
# Get code examples
|
|
405
|
+
context7.doc(library_key="pallets/flask", topic="blueprints", mode="code")
|
|
406
|
+
|
|
407
|
+
# Get version-specific docs
|
|
408
|
+
context7.doc(library_key="vercel/next.js", topic="routing", version="v14")
|
|
409
|
+
"""
|
|
410
|
+
# Validate mode parameter
|
|
411
|
+
if mode not in ("info", "code"):
|
|
412
|
+
return f"Invalid mode '{mode}'. Valid options: 'info', 'code'."
|
|
413
|
+
|
|
414
|
+
# Validate doc_type parameter
|
|
415
|
+
if doc_type not in ("txt", "json"):
|
|
416
|
+
return f"Invalid doc_type '{doc_type}'. Valid options: 'txt', 'json'."
|
|
417
|
+
|
|
418
|
+
with LogSpan(span="context7.doc", library_key=library_key, topic=topic, mode=mode) as s:
|
|
419
|
+
# Normalize and resolve library key (searches if needed)
|
|
420
|
+
resolved_key, was_searched, found_match = _resolve_library_key(library_key)
|
|
421
|
+
s.add(resolvedKey=resolved_key, wasSearched=was_searched, foundMatch=found_match)
|
|
422
|
+
|
|
423
|
+
# If search was performed but found no match, library doesn't exist
|
|
424
|
+
if was_searched and not found_match:
|
|
425
|
+
return (
|
|
426
|
+
f"Library '{library_key}' not found. "
|
|
427
|
+
f"Use context7.search(query=\"{library_key}\") to find available libraries."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Validate resolved key has org/repo format
|
|
431
|
+
if "/" not in resolved_key:
|
|
432
|
+
return (
|
|
433
|
+
f"Could not resolve library '{library_key}' to org/repo format. "
|
|
434
|
+
f"Please use full format like 'facebook/react' or 'vercel/next.js'. "
|
|
435
|
+
f"Use context7.search(query=\"{library_key}\") to find the correct library key."
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Append version suffix if provided
|
|
439
|
+
api_key = resolved_key
|
|
440
|
+
if version:
|
|
441
|
+
# Ensure version has 'v' prefix if it starts with a number
|
|
442
|
+
if version[0].isdigit():
|
|
443
|
+
version = f"v{version}"
|
|
444
|
+
api_key = f"{resolved_key}/{version}"
|
|
445
|
+
s.add(apiKey=api_key)
|
|
446
|
+
|
|
447
|
+
# Normalize topic
|
|
448
|
+
normalized_topic = _normalize_topic(topic)
|
|
449
|
+
|
|
450
|
+
# Clamp page and limit to valid ranges
|
|
451
|
+
config_docs_limit = _get_config().docs_limit
|
|
452
|
+
page = max(1, min(page, 10))
|
|
453
|
+
if limit is None:
|
|
454
|
+
limit = config_docs_limit
|
|
455
|
+
limit = max(1, min(limit, config_docs_limit))
|
|
456
|
+
|
|
457
|
+
# Select endpoint based on mode
|
|
458
|
+
base_url = CONTEXT7_DOCS_INFO_URL if mode == "info" else CONTEXT7_DOCS_CODE_URL
|
|
459
|
+
url = f"{base_url}/{api_key}"
|
|
460
|
+
params: dict[str, str | int] = {
|
|
461
|
+
"type": doc_type,
|
|
462
|
+
"page": page,
|
|
463
|
+
"limit": limit,
|
|
464
|
+
}
|
|
465
|
+
# Only include topic if non-empty
|
|
466
|
+
if normalized_topic:
|
|
467
|
+
params["topic"] = normalized_topic
|
|
468
|
+
|
|
469
|
+
success, data = _make_request(url, params=params)
|
|
470
|
+
s.add(success=success)
|
|
471
|
+
|
|
472
|
+
if not success:
|
|
473
|
+
return f"{data} library_key={library_key}"
|
|
474
|
+
|
|
475
|
+
# Handle response
|
|
476
|
+
if isinstance(data, str):
|
|
477
|
+
# Check for "no content" responses
|
|
478
|
+
if data in ("No content available", "No context data available", ""):
|
|
479
|
+
other_mode = "info" if mode == "code" else "code"
|
|
480
|
+
topic_hint = f" on topic '{topic}'" if topic else ""
|
|
481
|
+
return (
|
|
482
|
+
f"No {mode} documentation found for '{resolved_key}'{topic_hint}. "
|
|
483
|
+
f"Try mode='{other_mode}' or a different topic."
|
|
484
|
+
)
|
|
485
|
+
s.add(resultLen=len(data))
|
|
486
|
+
return data
|
|
487
|
+
|
|
488
|
+
if isinstance(data, dict):
|
|
489
|
+
result = str(data.get("content", data.get("text", str(data))))
|
|
490
|
+
s.add(resultLen=len(result))
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
result = str(data)
|
|
494
|
+
s.add(resultLen=len(result))
|
|
495
|
+
return result
|