synsc-context-proxy 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,90 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════
|
|
2
|
+
# SynSc Context — .gitignore
|
|
3
|
+
# ═══════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
# ─── Environment & secrets ────────────────────────────────────
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
!env.example
|
|
9
|
+
|
|
10
|
+
# ─── Python ───────────────────────────────────────────────────
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
*.so
|
|
15
|
+
build/
|
|
16
|
+
dist/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
*.egg
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
.python-version
|
|
23
|
+
|
|
24
|
+
# ─── Node / npm ───────────────────────────────────────────────
|
|
25
|
+
node_modules/
|
|
26
|
+
.pnp
|
|
27
|
+
.pnp.js
|
|
28
|
+
.yarn/install-state.gz
|
|
29
|
+
npm-debug.log*
|
|
30
|
+
yarn-debug.log*
|
|
31
|
+
yarn-error.log*
|
|
32
|
+
pnpm-debug.log*
|
|
33
|
+
|
|
34
|
+
# ─── Next.js (frontend/) ─────────────────────────────────────
|
|
35
|
+
.next/
|
|
36
|
+
out/
|
|
37
|
+
.vercel
|
|
38
|
+
|
|
39
|
+
# ─── TypeScript ───────────────────────────────────────────────
|
|
40
|
+
*.tsbuildinfo
|
|
41
|
+
|
|
42
|
+
# ─── Testing & coverage ──────────────────────────────────────
|
|
43
|
+
coverage/
|
|
44
|
+
htmlcov/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
*.cover
|
|
48
|
+
.pytest_cache/
|
|
49
|
+
.mypy_cache/
|
|
50
|
+
|
|
51
|
+
# ─── Linting / formatting cache ──────────────────────────────
|
|
52
|
+
.ruff_cache/
|
|
53
|
+
|
|
54
|
+
# ─── Data & local storage ────────────────────────────────────
|
|
55
|
+
data/
|
|
56
|
+
*.db
|
|
57
|
+
*.sqlite
|
|
58
|
+
*.sqlite3
|
|
59
|
+
|
|
60
|
+
# ─── Logs ─────────────────────────────────────────────────────
|
|
61
|
+
*.log
|
|
62
|
+
logs/
|
|
63
|
+
|
|
64
|
+
# ─── OS files ─────────────────────────────────────────────────
|
|
65
|
+
.DS_Store
|
|
66
|
+
Thumbs.db
|
|
67
|
+
Desktop.ini
|
|
68
|
+
|
|
69
|
+
# ─── IDE / editor ─────────────────────────────────────────────
|
|
70
|
+
.idea/
|
|
71
|
+
.vscode/
|
|
72
|
+
*.swp
|
|
73
|
+
*.swo
|
|
74
|
+
*~
|
|
75
|
+
|
|
76
|
+
# ─── Security ─────────────────────────────────────────────────
|
|
77
|
+
*.pem
|
|
78
|
+
*.key
|
|
79
|
+
*.cert
|
|
80
|
+
|
|
81
|
+
# ─── Docker (local volumes) ──────────────────────────────────
|
|
82
|
+
docker-data/
|
|
83
|
+
|
|
84
|
+
# ─── Supabase local ──────────────────────────────────────────
|
|
85
|
+
supabase/.temp/
|
|
86
|
+
|
|
87
|
+
# ─── Misc ─────────────────────────────────────────────────────
|
|
88
|
+
*.bak
|
|
89
|
+
*.tmp
|
|
90
|
+
*.orig
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: synsc-context-proxy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight MCP proxy for SyntheticSciences Context — gives AI agents deep code and research paper context.
|
|
5
|
+
Project-URL: Homepage, https://context.syntheticsciences.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/synthetic-sciences/synsc-context
|
|
7
|
+
Author: InkVell
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: ai,claude,code-search,cursor,mcp,research-papers,semantic-search
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: mcp[cli]>=1.4.1
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
Lightweight MCP proxy for [Synsc Context](https://context.syntheticsciences.ai). Gives AI agents (Claude, Cursor, etc.) deep code and research paper context via 20 tools. Only needs an API key — no heavy dependencies.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "synsc-context-proxy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight MCP proxy for SyntheticSciences Context — gives AI agents deep code and research paper context."
|
|
5
|
+
readme = {text = "Lightweight MCP proxy for [Synsc Context](https://context.syntheticsciences.ai). Gives AI agents (Claude, Cursor, etc.) deep code and research paper context via 20 tools. Only needs an API key — no heavy dependencies.", content-type = "text/markdown"}
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "InkVell" }
|
|
10
|
+
]
|
|
11
|
+
keywords = ["mcp", "ai", "code-search", "research-papers", "cursor", "claude", "semantic-search"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"mcp[cli]>=1.4.1",
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://context.syntheticsciences.ai"
|
|
32
|
+
Repository = "https://github.com/synthetic-sciences/synsc-context"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
synsc-context-proxy = "synsc_context_proxy:main"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["synsc_context_proxy"]
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight MCP stdio proxy for SyntheticSciences Context.
|
|
3
|
+
|
|
4
|
+
Runs locally as an MCP stdio server and proxies all tool calls to the
|
|
5
|
+
hosted backend API. Users only need SYNSC_API_KEY — no heavy dependencies,
|
|
6
|
+
no Supabase credentials required.
|
|
7
|
+
|
|
8
|
+
Setup (Claude Desktop / Cursor / Claude Code):
|
|
9
|
+
{
|
|
10
|
+
"mcpServers": {
|
|
11
|
+
"synsc-context": {
|
|
12
|
+
"command": "uvx",
|
|
13
|
+
"args": ["synsc-context-proxy"],
|
|
14
|
+
"env": { "SYNSC_API_KEY": "YOUR_API_KEY" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Get your API key at https://context.syntheticsciences.ai
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
from mcp.server.fastmcp import FastMCP
|
|
29
|
+
|
|
30
|
+
REMOTE_URL = os.environ.get(
|
|
31
|
+
"SYNSC_API_URL", "https://context.syntheticsciences.ai"
|
|
32
|
+
)
|
|
33
|
+
API_KEY = os.environ.get("SYNSC_API_KEY", "")
|
|
34
|
+
|
|
35
|
+
server = FastMCP("synsc-context")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _call_remote(tool_name: str, arguments: dict) -> dict[str, Any]:
|
|
39
|
+
"""Forward a tool call to the remote /mcp endpoint via JSON-RPC."""
|
|
40
|
+
if not API_KEY:
|
|
41
|
+
return {"success": False, "error": "SYNSC_API_KEY environment variable is not set."}
|
|
42
|
+
|
|
43
|
+
async with httpx.AsyncClient() as client:
|
|
44
|
+
resp = await client.post(
|
|
45
|
+
f"{REMOTE_URL}/mcp",
|
|
46
|
+
json={
|
|
47
|
+
"jsonrpc": "2.0",
|
|
48
|
+
"id": 1,
|
|
49
|
+
"method": "tools/call",
|
|
50
|
+
"params": {"name": tool_name, "arguments": arguments},
|
|
51
|
+
},
|
|
52
|
+
headers={"Authorization": f"Bearer {API_KEY}"},
|
|
53
|
+
timeout=120,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Handle HTTP errors (e.g. 401 Unauthorized)
|
|
57
|
+
if resp.status_code != 200:
|
|
58
|
+
detail = resp.text
|
|
59
|
+
try:
|
|
60
|
+
detail = resp.json().get("detail", resp.text)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return {"success": False, "error": f"HTTP {resp.status_code}: {detail}"}
|
|
64
|
+
|
|
65
|
+
data = resp.json()
|
|
66
|
+
|
|
67
|
+
if "error" in data:
|
|
68
|
+
return {"success": False, "error": data["error"].get("message", "Remote server error")}
|
|
69
|
+
|
|
70
|
+
# Extract text content from MCP response
|
|
71
|
+
content = data.get("result", {}).get("content", [])
|
|
72
|
+
for block in content:
|
|
73
|
+
if block.get("type") == "text":
|
|
74
|
+
try:
|
|
75
|
+
return json.loads(block["text"])
|
|
76
|
+
except (json.JSONDecodeError, TypeError):
|
|
77
|
+
return {"success": True, "result": block["text"]}
|
|
78
|
+
|
|
79
|
+
return {"success": True, "result": str(content)}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# REPOSITORY TOOLS
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@server.tool()
|
|
88
|
+
async def index_repository(url: str, branch: str = "main") -> dict[str, Any]:
|
|
89
|
+
"""Index a GitHub repository for semantic code search.
|
|
90
|
+
|
|
91
|
+
SMART DEDUPLICATION:
|
|
92
|
+
- If a PUBLIC repo is already indexed by someone else, it's instantly
|
|
93
|
+
added to your collection (no re-indexing needed!)
|
|
94
|
+
- If it's a NEW repo, it gets indexed and added to your collection
|
|
95
|
+
- PRIVATE repos are only accessible by you
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
url: GitHub repository URL (https://github.com/owner/repo) or shorthand (owner/repo)
|
|
99
|
+
branch: Branch to index (default: main)
|
|
100
|
+
"""
|
|
101
|
+
return await _call_remote("index_repository", {"url": url, "branch": branch})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@server.tool()
|
|
105
|
+
async def list_repositories(limit: int = 50, offset: int = 0) -> dict[str, Any]:
|
|
106
|
+
"""List repositories in your collection.
|
|
107
|
+
|
|
108
|
+
Shows all repos you've added (indexed or shared).
|
|
109
|
+
You can only search repos that are in your collection.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
limit: Maximum number of repositories to return (default: 50)
|
|
113
|
+
offset: Number of repositories to skip for pagination
|
|
114
|
+
"""
|
|
115
|
+
return await _call_remote("list_repositories", {"limit": limit, "offset": offset})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@server.tool()
|
|
119
|
+
async def get_repository(repo_id: str) -> dict[str, Any]:
|
|
120
|
+
"""Get detailed information about an indexed repository.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
repo_id: Repository identifier (UUID)
|
|
124
|
+
"""
|
|
125
|
+
return await _call_remote("get_repository", {"repo_id": repo_id})
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@server.tool()
|
|
129
|
+
async def delete_repository(repo_id: str) -> dict[str, Any]:
|
|
130
|
+
"""Permanently delete a repository from the index.
|
|
131
|
+
|
|
132
|
+
WARNING: This deletes ALL data including files, chunks, and embeddings.
|
|
133
|
+
For PUBLIC repos, this removes it for ALL users who added it!
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
repo_id: Repository identifier (UUID)
|
|
137
|
+
"""
|
|
138
|
+
return await _call_remote("delete_repository", {"repo_id": repo_id})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@server.tool()
|
|
142
|
+
async def remove_from_collection(repo_id: str) -> dict[str, Any]:
|
|
143
|
+
"""Remove a repository from your collection.
|
|
144
|
+
|
|
145
|
+
This removes the repo from YOUR searchable collection without deleting
|
|
146
|
+
the actual indexed data. The repo remains available for other users.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
repo_id: Repository identifier (UUID)
|
|
150
|
+
"""
|
|
151
|
+
return await _call_remote("remove_from_collection", {"repo_id": repo_id})
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# SEARCH TOOLS
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@server.tool()
|
|
160
|
+
async def search_code(
|
|
161
|
+
query: str,
|
|
162
|
+
repo_ids: list[str] | None = None,
|
|
163
|
+
language: str | None = None,
|
|
164
|
+
file_pattern: str | None = None,
|
|
165
|
+
top_k: int = 10,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Search code using natural language or keywords.
|
|
168
|
+
|
|
169
|
+
Uses semantic search (embeddings) to find relevant code snippets.
|
|
170
|
+
Results are ranked by relevance score.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
query: Natural language query or keywords
|
|
174
|
+
repo_ids: Optional list of repository IDs to limit search
|
|
175
|
+
language: Filter by programming language (e.g., "python", "typescript")
|
|
176
|
+
file_pattern: Glob pattern for file paths (e.g., "*.test.ts", "src/**/*.py")
|
|
177
|
+
top_k: Number of results to return (default: 10, max: 100)
|
|
178
|
+
"""
|
|
179
|
+
return await _call_remote(
|
|
180
|
+
"search_code",
|
|
181
|
+
{
|
|
182
|
+
"query": query,
|
|
183
|
+
"repo_ids": repo_ids,
|
|
184
|
+
"language": language,
|
|
185
|
+
"file_pattern": file_pattern,
|
|
186
|
+
"top_k": top_k,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@server.tool()
|
|
192
|
+
async def get_file(
|
|
193
|
+
repo_id: str,
|
|
194
|
+
file_path: str,
|
|
195
|
+
start_line: int | None = None,
|
|
196
|
+
end_line: int | None = None,
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
"""Get file content from an indexed repository.
|
|
199
|
+
|
|
200
|
+
Retrieves the full file or a specific line range.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
repo_id: Repository identifier
|
|
204
|
+
file_path: Path to file within repository (e.g., "src/main.py")
|
|
205
|
+
start_line: Starting line number (1-indexed, optional)
|
|
206
|
+
end_line: Ending line number (optional)
|
|
207
|
+
"""
|
|
208
|
+
return await _call_remote(
|
|
209
|
+
"get_file",
|
|
210
|
+
{
|
|
211
|
+
"repo_id": repo_id,
|
|
212
|
+
"file_path": file_path,
|
|
213
|
+
"start_line": start_line,
|
|
214
|
+
"end_line": end_line,
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# ANALYSIS TOOLS
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@server.tool()
|
|
225
|
+
async def analyze_repository(repo_id: str) -> dict[str, Any]:
|
|
226
|
+
"""Get comprehensive analysis of an indexed repository.
|
|
227
|
+
|
|
228
|
+
Provides deep understanding of a codebase including:
|
|
229
|
+
- Directory structure with annotations
|
|
230
|
+
- Entry points (main files, CLI, API endpoints)
|
|
231
|
+
- Dependencies from manifest files
|
|
232
|
+
- Framework detection
|
|
233
|
+
- Architecture pattern detection
|
|
234
|
+
- Key files (README, configs, CI/CD)
|
|
235
|
+
- Coding conventions
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
repo_id: Repository identifier (UUID)
|
|
239
|
+
"""
|
|
240
|
+
return await _call_remote("analyze_repository", {"repo_id": repo_id})
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@server.tool()
|
|
244
|
+
async def get_directory_structure(
|
|
245
|
+
repo_id: str,
|
|
246
|
+
max_depth: int = 4,
|
|
247
|
+
annotate: bool = True,
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
"""Get the directory structure of an indexed repository.
|
|
250
|
+
|
|
251
|
+
Returns a tree representation of the repository's file structure
|
|
252
|
+
with optional annotations explaining the purpose of each directory.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
repo_id: Repository identifier (UUID)
|
|
256
|
+
max_depth: Maximum directory depth to show (default: 4)
|
|
257
|
+
annotate: Whether to add purpose annotations to directories (default: true)
|
|
258
|
+
"""
|
|
259
|
+
return await _call_remote(
|
|
260
|
+
"get_directory_structure",
|
|
261
|
+
{"repo_id": repo_id, "max_depth": max_depth, "annotate": annotate},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# SYMBOL TOOLS
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@server.tool()
|
|
271
|
+
async def search_symbols(
|
|
272
|
+
name: str,
|
|
273
|
+
repo_ids: list[str] | None = None,
|
|
274
|
+
symbol_type: str | None = None,
|
|
275
|
+
language: str | None = None,
|
|
276
|
+
top_k: int = 20,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
"""Search for code symbols (functions, classes, methods) by name.
|
|
279
|
+
|
|
280
|
+
Finds functions, classes, and methods using partial name matching.
|
|
281
|
+
Use this to locate specific code constructs without knowing their exact location.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
name: Symbol name to search for (partial match supported)
|
|
285
|
+
repo_ids: Optional list of repository IDs to limit search
|
|
286
|
+
symbol_type: Filter by type: "function", "class", "method"
|
|
287
|
+
language: Filter by programming language (e.g., "python")
|
|
288
|
+
top_k: Maximum results to return (default: 20)
|
|
289
|
+
"""
|
|
290
|
+
return await _call_remote(
|
|
291
|
+
"search_symbols",
|
|
292
|
+
{
|
|
293
|
+
"name": name,
|
|
294
|
+
"repo_ids": repo_ids,
|
|
295
|
+
"symbol_type": symbol_type,
|
|
296
|
+
"language": language,
|
|
297
|
+
"top_k": top_k,
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@server.tool()
|
|
303
|
+
async def get_symbol(symbol_id: str) -> dict[str, Any]:
|
|
304
|
+
"""Get detailed information about a specific code symbol.
|
|
305
|
+
|
|
306
|
+
Returns complete symbol details including full docstring,
|
|
307
|
+
parameters with types, return type, and decorators.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
symbol_id: Symbol identifier (UUID from search_symbols)
|
|
311
|
+
"""
|
|
312
|
+
return await _call_remote("get_symbol", {"symbol_id": symbol_id})
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# RESEARCH PAPER TOOLS
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@server.tool()
|
|
321
|
+
async def index_paper(source: str) -> dict[str, Any]:
|
|
322
|
+
"""Index a research paper from arXiv or local PDF.
|
|
323
|
+
|
|
324
|
+
Supports multiple formats:
|
|
325
|
+
- arXiv URLs: "https://arxiv.org/abs/2401.12345"
|
|
326
|
+
- arXiv IDs: "2401.12345"
|
|
327
|
+
- Local PDF: "/path/to/paper.pdf"
|
|
328
|
+
|
|
329
|
+
Features global deduplication - if paper already indexed,
|
|
330
|
+
returns existing paper ID instantly.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
source: arXiv URL/ID or local PDF path
|
|
334
|
+
"""
|
|
335
|
+
return await _call_remote("index_paper", {"source": source})
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@server.tool()
|
|
339
|
+
async def list_papers(limit: int = 50, offset: int = 0) -> dict[str, Any]:
|
|
340
|
+
"""List all indexed papers.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
limit: Maximum papers to return (default: 50)
|
|
344
|
+
offset: Number of papers to skip for pagination
|
|
345
|
+
"""
|
|
346
|
+
return await _call_remote("list_papers", {"limit": limit, "offset": offset})
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@server.tool()
|
|
350
|
+
async def get_paper(paper_id: str) -> dict[str, Any]:
|
|
351
|
+
"""Get full paper content with all extracted features.
|
|
352
|
+
|
|
353
|
+
Returns complete paper data including sections, citations, equations,
|
|
354
|
+
and code snippets.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
paper_id: Paper identifier (UUID)
|
|
358
|
+
"""
|
|
359
|
+
return await _call_remote("get_paper", {"paper_id": paper_id})
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@server.tool()
|
|
363
|
+
async def search_papers(query: str, top_k: int = 5) -> dict[str, Any]:
|
|
364
|
+
"""Search papers using semantic search.
|
|
365
|
+
|
|
366
|
+
Uses embeddings to find relevant papers by meaning, not just keywords.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
query: Natural language search query
|
|
370
|
+
top_k: Number of results to return (default: 5)
|
|
371
|
+
"""
|
|
372
|
+
return await _call_remote("search_papers", {"query": query, "top_k": top_k})
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@server.tool()
|
|
376
|
+
async def get_citations(paper_id: str) -> dict[str, Any]:
|
|
377
|
+
"""Extract all citations from a paper.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
paper_id: Paper identifier (UUID)
|
|
381
|
+
"""
|
|
382
|
+
return await _call_remote("get_citations", {"paper_id": paper_id})
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@server.tool()
|
|
386
|
+
async def get_equations(paper_id: str) -> dict[str, Any]:
|
|
387
|
+
"""Extract all equations from a paper.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
paper_id: Paper identifier (UUID)
|
|
391
|
+
"""
|
|
392
|
+
return await _call_remote("get_equations", {"paper_id": paper_id})
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@server.tool()
|
|
396
|
+
async def get_code_snippets(paper_id: str) -> dict[str, Any]:
|
|
397
|
+
"""Extract all code snippets from a paper.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
paper_id: Paper identifier (UUID)
|
|
401
|
+
"""
|
|
402
|
+
return await _call_remote("get_code_snippets", {"paper_id": paper_id})
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@server.tool()
|
|
406
|
+
async def generate_report(paper_id: str) -> dict[str, Any]:
|
|
407
|
+
"""Generate comprehensive markdown report for a paper.
|
|
408
|
+
|
|
409
|
+
Creates a detailed report optimized for LLM consumption including
|
|
410
|
+
content, citations, and equations.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
paper_id: Paper identifier (UUID)
|
|
414
|
+
"""
|
|
415
|
+
return await _call_remote("generate_report", {"paper_id": paper_id})
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@server.tool()
|
|
419
|
+
async def compare_papers(paper_ids: list[str]) -> dict[str, Any]:
|
|
420
|
+
"""Compare multiple papers side-by-side.
|
|
421
|
+
|
|
422
|
+
Provides comparison including common themes and differences.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
paper_ids: List of 2-5 paper identifiers (UUIDs)
|
|
426
|
+
"""
|
|
427
|
+
return await _call_remote("compare_papers", {"paper_ids": paper_ids})
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def main():
|
|
431
|
+
"""Entry point for synsc-context-proxy CLI."""
|
|
432
|
+
if not API_KEY:
|
|
433
|
+
print(
|
|
434
|
+
"Error: SYNSC_API_KEY environment variable is not set.\n"
|
|
435
|
+
"Get your API key from https://context.syntheticsciences.ai",
|
|
436
|
+
file=sys.stderr,
|
|
437
|
+
)
|
|
438
|
+
sys.exit(1)
|
|
439
|
+
|
|
440
|
+
server.run(transport="stdio")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
main()
|