crossref-local 0.3.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crossref_local/__init__.py +38 -16
- crossref_local/__main__.py +0 -0
- crossref_local/_aio/__init__.py +30 -0
- crossref_local/_aio/_impl.py +238 -0
- crossref_local/_cache/__init__.py +15 -0
- crossref_local/_cache/export.py +100 -0
- crossref_local/_cache/utils.py +93 -0
- crossref_local/_cache/viz.py +296 -0
- crossref_local/_cli/__init__.py +9 -0
- crossref_local/_cli/cache.py +179 -0
- crossref_local/_cli/cli.py +512 -0
- crossref_local/_cli/completion.py +245 -0
- crossref_local/_cli/main.py +20 -0
- crossref_local/_cli/mcp.py +351 -0
- crossref_local/_cli/mcp_server.py +413 -0
- crossref_local/_core/__init__.py +58 -0
- crossref_local/{api.py → _core/api.py} +130 -36
- crossref_local/{citations.py → _core/citations.py} +55 -26
- crossref_local/{config.py → _core/config.py} +57 -42
- crossref_local/{db.py → _core/db.py} +32 -26
- crossref_local/{fts.py → _core/fts.py} +18 -14
- crossref_local/{models.py → _core/models.py} +11 -6
- crossref_local/{impact_factor → _impact_factor}/__init__.py +0 -0
- crossref_local/{impact_factor → _impact_factor}/calculator.py +0 -0
- crossref_local/{impact_factor → _impact_factor}/journal_lookup.py +0 -0
- crossref_local/_remote/__init__.py +56 -0
- crossref_local/_remote/base.py +356 -0
- crossref_local/_remote/collections.py +175 -0
- crossref_local/_server/__init__.py +140 -0
- crossref_local/_server/middleware.py +25 -0
- crossref_local/_server/models.py +129 -0
- crossref_local/_server/routes_citations.py +98 -0
- crossref_local/_server/routes_collections.py +282 -0
- crossref_local/_server/routes_compat.py +102 -0
- crossref_local/_server/routes_works.py +128 -0
- crossref_local/_server/server.py +19 -0
- crossref_local/aio.py +30 -206
- crossref_local/cache.py +466 -0
- crossref_local/cli.py +5 -447
- crossref_local/jobs.py +169 -0
- crossref_local/mcp_server.py +5 -199
- crossref_local/remote.py +5 -261
- crossref_local/server.py +5 -349
- {crossref_local-0.3.1.dist-info → crossref_local-0.5.0.dist-info}/METADATA +88 -24
- crossref_local-0.5.0.dist-info/RECORD +47 -0
- crossref_local-0.3.1.dist-info/RECORD +0 -20
- {crossref_local-0.3.1.dist-info → crossref_local-0.5.0.dist-info}/WHEEL +0 -0
- {crossref_local-0.3.1.dist-info → crossref_local-0.5.0.dist-info}/entry_points.txt +0 -0
crossref_local/server.py
CHANGED
|
@@ -1,352 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Backward compatibility: re-export from _server."""
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
unlike the Django API which only scans a limited subset.
|
|
4
|
+
from ._server import app, run_server, DEFAULT_PORT, DEFAULT_HOST
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
crossref-local api # Run on default port 3333
|
|
8
|
-
crossref-local api --port 8080 # Custom port
|
|
6
|
+
__all__ = ["app", "run_server", "DEFAULT_PORT", "DEFAULT_HOST"]
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
uvicorn crossref_local.server:app --host 0.0.0.0 --port 3333
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import time
|
|
15
|
-
from typing import Optional, List
|
|
16
|
-
|
|
17
|
-
from fastapi import FastAPI, Query, HTTPException
|
|
18
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
19
|
-
from pydantic import BaseModel
|
|
20
|
-
|
|
21
|
-
from . import fts
|
|
22
|
-
from .db import get_db
|
|
23
|
-
from .models import Work
|
|
24
|
-
|
|
25
|
-
app = FastAPI(
|
|
26
|
-
title="CrossRef Local API",
|
|
27
|
-
description="Fast full-text search across 167M+ scholarly works",
|
|
28
|
-
version="1.1.0",
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
# CORS middleware
|
|
32
|
-
app.add_middleware(
|
|
33
|
-
CORSMiddleware,
|
|
34
|
-
allow_origins=["*"],
|
|
35
|
-
allow_methods=["*"],
|
|
36
|
-
allow_headers=["*"],
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class WorkResponse(BaseModel):
|
|
41
|
-
doi: str
|
|
42
|
-
title: Optional[str] = None
|
|
43
|
-
authors: List[str] = []
|
|
44
|
-
year: Optional[int] = None
|
|
45
|
-
journal: Optional[str] = None
|
|
46
|
-
issn: Optional[str] = None
|
|
47
|
-
volume: Optional[str] = None
|
|
48
|
-
issue: Optional[str] = None
|
|
49
|
-
page: Optional[str] = None
|
|
50
|
-
abstract: Optional[str] = None
|
|
51
|
-
citation_count: Optional[int] = None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class SearchResponse(BaseModel):
|
|
55
|
-
query: str
|
|
56
|
-
total: int
|
|
57
|
-
returned: int
|
|
58
|
-
elapsed_ms: float
|
|
59
|
-
results: List[WorkResponse]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class InfoResponse(BaseModel):
|
|
63
|
-
name: str = "CrossRef Local API"
|
|
64
|
-
version: str = "1.1.0"
|
|
65
|
-
status: str = "running"
|
|
66
|
-
mode: str = "local"
|
|
67
|
-
total_papers: int
|
|
68
|
-
fts_indexed: int
|
|
69
|
-
citations: int
|
|
70
|
-
database_path: str
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@app.get("/")
|
|
74
|
-
def root():
|
|
75
|
-
"""API root with endpoint information."""
|
|
76
|
-
return {
|
|
77
|
-
"name": "CrossRef Local API",
|
|
78
|
-
"version": "1.1.0",
|
|
79
|
-
"status": "running",
|
|
80
|
-
"endpoints": {
|
|
81
|
-
"health": "/health",
|
|
82
|
-
"info": "/info",
|
|
83
|
-
"search": "/works?q=<query>",
|
|
84
|
-
"get_by_doi": "/works/{doi}",
|
|
85
|
-
"batch": "/works/batch",
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@app.get("/health")
|
|
91
|
-
def health():
|
|
92
|
-
"""Health check endpoint."""
|
|
93
|
-
db = get_db()
|
|
94
|
-
return {
|
|
95
|
-
"status": "healthy",
|
|
96
|
-
"database_connected": db is not None,
|
|
97
|
-
"database_path": str(db.db_path) if db else None,
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@app.get("/info", response_model=InfoResponse)
|
|
102
|
-
def info():
|
|
103
|
-
"""Get database statistics."""
|
|
104
|
-
db = get_db()
|
|
105
|
-
|
|
106
|
-
row = db.fetchone("SELECT COUNT(*) as count FROM works")
|
|
107
|
-
work_count = row["count"] if row else 0
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
row = db.fetchone("SELECT COUNT(*) as count FROM works_fts")
|
|
111
|
-
fts_count = row["count"] if row else 0
|
|
112
|
-
except Exception:
|
|
113
|
-
fts_count = 0
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
row = db.fetchone("SELECT COUNT(*) as count FROM citations")
|
|
117
|
-
citation_count = row["count"] if row else 0
|
|
118
|
-
except Exception:
|
|
119
|
-
citation_count = 0
|
|
120
|
-
|
|
121
|
-
return InfoResponse(
|
|
122
|
-
total_papers=work_count,
|
|
123
|
-
fts_indexed=fts_count,
|
|
124
|
-
citations=citation_count,
|
|
125
|
-
database_path=str(db.db_path),
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
@app.get("/works", response_model=SearchResponse)
|
|
130
|
-
def search_works(
|
|
131
|
-
q: str = Query(..., description="Search query (FTS5 syntax supported)"),
|
|
132
|
-
limit: int = Query(10, ge=1, le=100, description="Max results"),
|
|
133
|
-
offset: int = Query(0, ge=0, description="Skip first N results"),
|
|
134
|
-
):
|
|
135
|
-
"""
|
|
136
|
-
Full-text search across works.
|
|
137
|
-
|
|
138
|
-
Uses FTS5 index for fast searching across titles, abstracts, and authors.
|
|
139
|
-
Supports FTS5 query syntax like AND, OR, NOT, "exact phrases".
|
|
140
|
-
|
|
141
|
-
Examples:
|
|
142
|
-
/works?q=machine learning
|
|
143
|
-
/works?q="neural network" AND hippocampus
|
|
144
|
-
/works?q=CRISPR&limit=20
|
|
145
|
-
"""
|
|
146
|
-
start = time.perf_counter()
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
results = fts.search(q, limit=limit, offset=offset)
|
|
150
|
-
except Exception as e:
|
|
151
|
-
raise HTTPException(status_code=400, detail=f"Search error: {e}")
|
|
152
|
-
|
|
153
|
-
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
154
|
-
|
|
155
|
-
return SearchResponse(
|
|
156
|
-
query=q,
|
|
157
|
-
total=results.total,
|
|
158
|
-
returned=len(results.works),
|
|
159
|
-
elapsed_ms=round(elapsed_ms, 2),
|
|
160
|
-
results=[
|
|
161
|
-
WorkResponse(
|
|
162
|
-
doi=w.doi,
|
|
163
|
-
title=w.title,
|
|
164
|
-
authors=w.authors,
|
|
165
|
-
year=w.year,
|
|
166
|
-
journal=w.journal,
|
|
167
|
-
issn=w.issn,
|
|
168
|
-
volume=w.volume,
|
|
169
|
-
issue=w.issue,
|
|
170
|
-
page=w.page,
|
|
171
|
-
abstract=w.abstract,
|
|
172
|
-
citation_count=w.citation_count,
|
|
173
|
-
)
|
|
174
|
-
for w in results.works
|
|
175
|
-
],
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
@app.get("/works/{doi:path}", response_model=Optional[WorkResponse])
|
|
180
|
-
def get_work(doi: str):
|
|
181
|
-
"""
|
|
182
|
-
Get work metadata by DOI.
|
|
183
|
-
|
|
184
|
-
Examples:
|
|
185
|
-
/works/10.1038/nature12373
|
|
186
|
-
/works/10.1016/j.cell.2020.01.001
|
|
187
|
-
"""
|
|
188
|
-
db = get_db()
|
|
189
|
-
metadata = db.get_metadata(doi)
|
|
190
|
-
|
|
191
|
-
if metadata is None:
|
|
192
|
-
raise HTTPException(status_code=404, detail=f"DOI not found: {doi}")
|
|
193
|
-
|
|
194
|
-
work = Work.from_metadata(doi, metadata)
|
|
195
|
-
|
|
196
|
-
return WorkResponse(
|
|
197
|
-
doi=work.doi,
|
|
198
|
-
title=work.title,
|
|
199
|
-
authors=work.authors,
|
|
200
|
-
year=work.year,
|
|
201
|
-
journal=work.journal,
|
|
202
|
-
issn=work.issn,
|
|
203
|
-
volume=work.volume,
|
|
204
|
-
issue=work.issue,
|
|
205
|
-
page=work.page,
|
|
206
|
-
abstract=work.abstract,
|
|
207
|
-
citation_count=work.citation_count,
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class BatchRequest(BaseModel):
|
|
212
|
-
dois: List[str]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
class BatchResponse(BaseModel):
|
|
216
|
-
requested: int
|
|
217
|
-
found: int
|
|
218
|
-
results: List[WorkResponse]
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@app.post("/works/batch", response_model=BatchResponse)
|
|
222
|
-
def get_works_batch(request: BatchRequest):
|
|
223
|
-
"""
|
|
224
|
-
Get multiple works by DOI.
|
|
225
|
-
|
|
226
|
-
Request body: {"dois": ["10.1038/...", "10.1016/..."]}
|
|
227
|
-
"""
|
|
228
|
-
db = get_db()
|
|
229
|
-
results = []
|
|
230
|
-
|
|
231
|
-
for doi in request.dois:
|
|
232
|
-
metadata = db.get_metadata(doi)
|
|
233
|
-
if metadata:
|
|
234
|
-
work = Work.from_metadata(doi, metadata)
|
|
235
|
-
results.append(
|
|
236
|
-
WorkResponse(
|
|
237
|
-
doi=work.doi,
|
|
238
|
-
title=work.title,
|
|
239
|
-
authors=work.authors,
|
|
240
|
-
year=work.year,
|
|
241
|
-
journal=work.journal,
|
|
242
|
-
abstract=work.abstract,
|
|
243
|
-
citation_count=work.citation_count,
|
|
244
|
-
)
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
return BatchResponse(
|
|
248
|
-
requested=len(request.dois),
|
|
249
|
-
found=len(results),
|
|
250
|
-
results=results,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
# For backwards compatibility with existing API endpoints
|
|
255
|
-
@app.get("/api/search/")
|
|
256
|
-
def api_search_compat(
|
|
257
|
-
title: Optional[str] = None,
|
|
258
|
-
q: Optional[str] = None,
|
|
259
|
-
doi: Optional[str] = None,
|
|
260
|
-
limit: int = 10,
|
|
261
|
-
):
|
|
262
|
-
"""Backwards-compatible search endpoint."""
|
|
263
|
-
query = title or q
|
|
264
|
-
|
|
265
|
-
if doi:
|
|
266
|
-
# DOI lookup
|
|
267
|
-
try:
|
|
268
|
-
work = get_work(doi)
|
|
269
|
-
return {
|
|
270
|
-
"query": {"doi": doi},
|
|
271
|
-
"results": [work.model_dump()],
|
|
272
|
-
"total": 1,
|
|
273
|
-
"returned": 1,
|
|
274
|
-
}
|
|
275
|
-
except HTTPException:
|
|
276
|
-
return {"query": {"doi": doi}, "results": [], "total": 0, "returned": 0}
|
|
277
|
-
|
|
278
|
-
if not query:
|
|
279
|
-
raise HTTPException(
|
|
280
|
-
status_code=400, detail="Specify q, title, or doi parameter"
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
# Call fts.search directly (not the endpoint function)
|
|
284
|
-
results = fts.search(query, limit=limit, offset=0)
|
|
285
|
-
return {
|
|
286
|
-
"query": {
|
|
287
|
-
"title": query,
|
|
288
|
-
"doi": None,
|
|
289
|
-
"year": None,
|
|
290
|
-
"authors": None,
|
|
291
|
-
"limit": limit,
|
|
292
|
-
},
|
|
293
|
-
"results": [
|
|
294
|
-
WorkResponse(
|
|
295
|
-
doi=w.doi,
|
|
296
|
-
title=w.title,
|
|
297
|
-
authors=w.authors,
|
|
298
|
-
year=w.year,
|
|
299
|
-
journal=w.journal,
|
|
300
|
-
issn=w.issn,
|
|
301
|
-
volume=w.volume,
|
|
302
|
-
issue=w.issue,
|
|
303
|
-
page=w.page,
|
|
304
|
-
abstract=w.abstract,
|
|
305
|
-
citation_count=w.citation_count,
|
|
306
|
-
).model_dump()
|
|
307
|
-
for w in results.works
|
|
308
|
-
],
|
|
309
|
-
"total": results.total,
|
|
310
|
-
"returned": len(results.works),
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
@app.get("/api/stats/")
|
|
315
|
-
def api_stats_compat():
|
|
316
|
-
"""Backwards-compatible stats endpoint."""
|
|
317
|
-
db = get_db()
|
|
318
|
-
|
|
319
|
-
row = db.fetchone("SELECT COUNT(*) as count FROM works")
|
|
320
|
-
work_count = row["count"] if row else 0
|
|
321
|
-
|
|
322
|
-
# Get table names
|
|
323
|
-
tables = []
|
|
324
|
-
for row in db.fetchall("SELECT name FROM sqlite_master WHERE type='table'"):
|
|
325
|
-
tables.append(row["name"])
|
|
326
|
-
|
|
327
|
-
# Get index names
|
|
328
|
-
indices = []
|
|
329
|
-
for row in db.fetchall("SELECT name FROM sqlite_master WHERE type='index'"):
|
|
330
|
-
if row["name"]:
|
|
331
|
-
indices.append(row["name"])
|
|
332
|
-
|
|
333
|
-
return {
|
|
334
|
-
"total_papers": work_count,
|
|
335
|
-
"database_size_mb": None,
|
|
336
|
-
"year_range": None,
|
|
337
|
-
"total_journals": 0,
|
|
338
|
-
"total_citations": None,
|
|
339
|
-
"tables": tables,
|
|
340
|
-
"indices": indices,
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def run_server(host: str = "0.0.0.0", port: int = 3333):
|
|
345
|
-
"""Run the FastAPI server."""
|
|
346
|
-
import uvicorn
|
|
347
|
-
|
|
348
|
-
uvicorn.run(app, host=host, port=port)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if __name__ == "__main__":
|
|
352
|
-
run_server()
|
|
8
|
+
# EOF
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crossref-local
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Local CrossRef database with 167M+ works and full-text search
|
|
5
5
|
Project-URL: Homepage, https://github.com/ywatanabe1989/crossref_local
|
|
6
6
|
Project-URL: Repository, https://github.com/ywatanabe1989/crossref_local
|
|
@@ -18,6 +18,7 @@ Classifier: Topic :: Database
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: click>=8.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
21
22
|
Provides-Extra: all
|
|
22
23
|
Requires-Dist: fastapi>=0.100; extra == 'all'
|
|
23
24
|
Requires-Dist: fastmcp>=0.4; extra == 'all'
|
|
@@ -43,11 +44,20 @@ Requires-Dist: networkx>=3.0; extra == 'viz'
|
|
|
43
44
|
Requires-Dist: pyvis>=0.3; extra == 'viz'
|
|
44
45
|
Description-Content-Type: text/markdown
|
|
45
46
|
|
|
47
|
+
<!-- ---
|
|
48
|
+
!-- Timestamp: 2026-01-16 19:15:51
|
|
49
|
+
!-- Author: ywatanabe
|
|
50
|
+
!-- File: /home/ywatanabe/proj/crossref-local/README.md
|
|
51
|
+
!-- --- -->
|
|
52
|
+
|
|
46
53
|
# CrossRef Local
|
|
47
54
|
|
|
48
55
|
Local CrossRef database with 167M+ scholarly works, full-text search, and impact factor calculation.
|
|
49
56
|
|
|
57
|
+
[](https://badge.fury.io/py/crossref-local)
|
|
58
|
+
[](https://crossref-local.readthedocs.io/en/latest/)
|
|
50
59
|
[](https://github.com/ywatanabe1989/crossref-local/actions/workflows/test.yml)
|
|
60
|
+
[](https://codecov.io/gh/ywatanabe1989/crossref-local)
|
|
51
61
|
[](https://www.python.org/downloads/)
|
|
52
62
|
[](LICENSE)
|
|
53
63
|
|
|
@@ -55,6 +65,22 @@ Local CrossRef database with 167M+ scholarly works, full-text search, and impact
|
|
|
55
65
|
<img src="examples/readme_figure.png" alt="CrossRef Local Demo" width="800"/>
|
|
56
66
|
</p>
|
|
57
67
|
|
|
68
|
+
<details>
|
|
69
|
+
<summary><strong>MCP Demo Video</strong></summary>
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<a href="https://scitex.ai/media/videos/crossref-local-v0.3.1-demo.mp4">
|
|
73
|
+
<img src="examples/demo_mcp_out/crossref-local-v0.3.1-demo-thumbnail_6m55s.png" alt="Demo Video Thumbnail" width="600"/>
|
|
74
|
+
</a>
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
Live demonstration of MCP server integration with Claude Code for `epilepsy seizure prediction` literature review:
|
|
78
|
+
- Full-text search on title, abstracts, and keywords across 167M papers (22ms response)
|
|
79
|
+
|
|
80
|
+
📄 [Full demo documentation](examples/demo_mcp.org) | 📊 [Generated diagrams](examples/demo_mcp_out/)
|
|
81
|
+
|
|
82
|
+
</details>
|
|
83
|
+
|
|
58
84
|
<details>
|
|
59
85
|
<summary><strong>Why CrossRef Local?</strong></summary>
|
|
60
86
|
|
|
@@ -135,9 +161,8 @@ async def main():
|
|
|
135
161
|
|
|
136
162
|
```bash
|
|
137
163
|
crossref-local search "CRISPR genome editing" -n 5
|
|
138
|
-
crossref-local
|
|
139
|
-
crossref-local
|
|
140
|
-
crossref-local info # Database stats
|
|
164
|
+
crossref-local search-by-doi 10.1038/nature12373
|
|
165
|
+
crossref-local status # Configuration and database stats
|
|
141
166
|
```
|
|
142
167
|
|
|
143
168
|
With abstracts (`-a` flag):
|
|
@@ -160,53 +185,68 @@ Found 4 matches in 128.4ms
|
|
|
160
185
|
|
|
161
186
|
Start the FastAPI server:
|
|
162
187
|
```bash
|
|
163
|
-
crossref-local
|
|
188
|
+
crossref-local relay --host 0.0.0.0 --port 31291
|
|
164
189
|
```
|
|
165
190
|
|
|
166
191
|
Endpoints:
|
|
167
192
|
```bash
|
|
168
193
|
# Search works (FTS5)
|
|
169
|
-
curl "http://localhost:
|
|
194
|
+
curl "http://localhost:31291/works?q=CRISPR&limit=10"
|
|
170
195
|
|
|
171
196
|
# Get by DOI
|
|
172
|
-
curl "http://localhost:
|
|
197
|
+
curl "http://localhost:31291/works/10.1038/nature12373"
|
|
173
198
|
|
|
174
199
|
# Batch DOI lookup
|
|
175
|
-
curl -X POST "http://localhost:
|
|
200
|
+
curl -X POST "http://localhost:31291/works/batch" \
|
|
176
201
|
-H "Content-Type: application/json" \
|
|
177
202
|
-d '{"dois": ["10.1038/nature12373", "10.1126/science.aax0758"]}'
|
|
178
203
|
|
|
204
|
+
# Citation endpoints
|
|
205
|
+
curl "http://localhost:31291/citations/10.1038/nature12373/citing"
|
|
206
|
+
curl "http://localhost:31291/citations/10.1038/nature12373/cited"
|
|
207
|
+
curl "http://localhost:31291/citations/10.1038/nature12373/count"
|
|
208
|
+
|
|
209
|
+
# Collection endpoints
|
|
210
|
+
curl "http://localhost:31291/collections"
|
|
211
|
+
curl -X POST "http://localhost:31291/collections" \
|
|
212
|
+
-H "Content-Type: application/json" \
|
|
213
|
+
-d '{"name": "my_papers", "query": "CRISPR", "limit": 100}'
|
|
214
|
+
curl "http://localhost:31291/collections/my_papers/download?format=bibtex"
|
|
215
|
+
|
|
179
216
|
# Database info
|
|
180
|
-
curl "http://localhost:
|
|
217
|
+
curl "http://localhost:31291/info"
|
|
181
218
|
```
|
|
182
219
|
|
|
183
|
-
|
|
220
|
+
HTTP mode (connect to running server):
|
|
184
221
|
```bash
|
|
185
|
-
# On local machine
|
|
186
|
-
ssh -L
|
|
222
|
+
# On local machine (if server is remote)
|
|
223
|
+
ssh -L 31291:127.0.0.1:31291 your-server
|
|
187
224
|
|
|
188
225
|
# Python client
|
|
189
|
-
from crossref_local import
|
|
190
|
-
|
|
226
|
+
from crossref_local import configure_http
|
|
227
|
+
configure_http("http://localhost:31291")
|
|
228
|
+
|
|
229
|
+
# Or via CLI
|
|
230
|
+
crossref-local --http search "CRISPR"
|
|
191
231
|
```
|
|
192
232
|
|
|
193
233
|
</details>
|
|
194
234
|
|
|
195
235
|
<details>
|
|
196
|
-
<summary><strong>MCP Server
|
|
236
|
+
<summary><strong>MCP Server</strong></summary>
|
|
197
237
|
|
|
198
|
-
Run as MCP
|
|
238
|
+
Run as MCP (Model Context Protocol) server:
|
|
199
239
|
```bash
|
|
200
|
-
crossref-local
|
|
240
|
+
crossref-local mcp start
|
|
201
241
|
```
|
|
202
242
|
|
|
203
|
-
|
|
243
|
+
Local MCP client configuration:
|
|
204
244
|
```json
|
|
205
245
|
{
|
|
206
246
|
"mcpServers": {
|
|
207
247
|
"crossref-local": {
|
|
208
248
|
"command": "crossref-local",
|
|
209
|
-
"args": ["
|
|
249
|
+
"args": ["mcp", "start"],
|
|
210
250
|
"env": {
|
|
211
251
|
"CROSSREF_LOCAL_DB": "/path/to/crossref.db"
|
|
212
252
|
}
|
|
@@ -215,12 +255,36 @@ Add to Claude Desktop config (`~/.config/claude/claude_desktop_config.json`):
|
|
|
215
255
|
}
|
|
216
256
|
```
|
|
217
257
|
|
|
258
|
+
Remote MCP via HTTP (recommended):
|
|
259
|
+
```bash
|
|
260
|
+
# On server: start persistent MCP server
|
|
261
|
+
crossref-local mcp start -t http --host 0.0.0.0 --port 8082
|
|
262
|
+
```
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"mcpServers": {
|
|
266
|
+
"crossref-remote": {
|
|
267
|
+
"url": "http://your-server:8082/mcp"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Diagnose setup:
|
|
274
|
+
```bash
|
|
275
|
+
crossref-local mcp doctor # Check dependencies and database
|
|
276
|
+
crossref-local mcp list-tools # Show available MCP tools
|
|
277
|
+
crossref-local mcp installation # Show client config examples
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
See [docs/remote-deployment.md](docs/remote-deployment.md) for systemd and Docker setup.
|
|
281
|
+
|
|
218
282
|
Available tools:
|
|
219
|
-
- `
|
|
220
|
-
- `
|
|
221
|
-
- `
|
|
222
|
-
- `
|
|
223
|
-
- `
|
|
283
|
+
- `search` - Full-text search across 167M+ papers
|
|
284
|
+
- `search_by_doi` - Get paper by DOI
|
|
285
|
+
- `enrich_dois` - Add citation counts and references to DOIs
|
|
286
|
+
- `status` - Database statistics
|
|
287
|
+
- `cache_*` - Paper collection management
|
|
224
288
|
|
|
225
289
|
</details>
|
|
226
290
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
crossref_local/__init__.py,sha256=bOn6qJob4okwkLzzIoEGQj0I-amIBSr89iWrbkYEJns,3691
|
|
2
|
+
crossref_local/__main__.py,sha256=N1c1ESGgJkAwsSWXANUgmzxC1OJEIqw-cl9m4pmNP7s,110
|
|
3
|
+
crossref_local/aio.py,sha256=_vDHFoE2n6GqO3j6pGeCaJVVe5I5L3M0tuMlW8IgGx8,1255
|
|
4
|
+
crossref_local/cache.py,sha256=yQpnRRBsHx2pluIPPfJlrakeuCC8TysKXrfA3ohdxSg,13321
|
|
5
|
+
crossref_local/cli.py,sha256=2pWJK_vnO1IIwUrMB3K8KW94ZWfkEF7TLSxMsaR-LrI,137
|
|
6
|
+
crossref_local/jobs.py,sha256=1tyaFRjyQGVi8xqk5nYga7maIFq2qYTroBGwAeN5DFc,4835
|
|
7
|
+
crossref_local/mcp_server.py,sha256=YmUd3xKqSDnW6sYDC7KRpZ7sSTnbTnxxGw9s_5lqIt8,185
|
|
8
|
+
crossref_local/remote.py,sha256=PmvUq87mC76sM9BL9RczOaXtvwHoqMc_dN5PJCYT18M,239
|
|
9
|
+
crossref_local/server.py,sha256=SKoQ-cOoZjdXm24Sv1CFu3F8UclbD6QstfQb-7l2xtA,215
|
|
10
|
+
crossref_local/_aio/__init__.py,sha256=z5_RNd9PTQUslI5LWOCn0711s8cIRX7N-Y-S-JyutAU,371
|
|
11
|
+
crossref_local/_aio/_impl.py,sha256=uG5r0d92EcPDqB52w4Wjb7kf1GmjHrLEELHINmNXnJc,5626
|
|
12
|
+
crossref_local/_cache/__init__.py,sha256=QSSTVWB_zNU3JQRdnBCzHuIl93WM3udgO2NkRlgm9M0,272
|
|
13
|
+
crossref_local/_cache/export.py,sha256=ZyZS1vWZE9ui9weywejnBFI2FzNV1r1Q_3XXPiFIj3M,3028
|
|
14
|
+
crossref_local/_cache/utils.py,sha256=D3sg7MoXZs4aC2OvagVgmOVMZjm1Xqy1ulGbs0_F914,2502
|
|
15
|
+
crossref_local/_cache/viz.py,sha256=0VAHK-i1xR5StEYrJ_IAs0HSk0MQNVFi4EjPtlZEwTY,8498
|
|
16
|
+
crossref_local/_cli/__init__.py,sha256=NS07Eo93dRAuO5vxGwGwS0ctvZxMMkF_py09bzIk3Hk,175
|
|
17
|
+
crossref_local/_cli/cache.py,sha256=8QykJ7-H313x7oKRw5fJ9Coay27Y8uBWGx5JR1VQ1_Y,6220
|
|
18
|
+
crossref_local/_cli/cli.py,sha256=w7rJ4ZtetSr6TRz2f2vyHqZylq5vXM8O9KB61gScbFM,16903
|
|
19
|
+
crossref_local/_cli/completion.py,sha256=yzqwMbGbbqXJQHdztuaqji-AdFTd8zDDhHeNGdw4NpU,7341
|
|
20
|
+
crossref_local/_cli/main.py,sha256=NgCGB5-ThofTRPoxxwZpsfCQTo5WIw8oP7zBSKhEiDQ,444
|
|
21
|
+
crossref_local/_cli/mcp.py,sha256=yjUoq3hvu2OofYf_D29_NXoTvkhxz5QbWAuWv6U-H8U,11427
|
|
22
|
+
crossref_local/_cli/mcp_server.py,sha256=jZJwhiTavgTHcJctXpafPgy3aIT49-rdIZ-4i88myeU,10989
|
|
23
|
+
crossref_local/_core/__init__.py,sha256=6xKO9y3VOuTF1f83uyQ48HWhszYpPfWZj94t0q0rEgk,940
|
|
24
|
+
crossref_local/_core/api.py,sha256=YogfhBHDIqT-hdEI-iDe_-py13Vp6F4GW8q0psLDRyQ,7781
|
|
25
|
+
crossref_local/_core/citations.py,sha256=4ERCTVkZu6kesZRazaWcTpUUeYz0rs97NwFZAYcLdKA,12765
|
|
26
|
+
crossref_local/_core/config.py,sha256=T-2EX4EDoCLaOAmViBKGQuNvoJP8x7cvZOvmAcNrQms,5475
|
|
27
|
+
crossref_local/_core/db.py,sha256=KLlcdNWLnKSexc0cuRZGsBLmJUnXeEaD_CBcXhbPXLg,3832
|
|
28
|
+
crossref_local/_core/fts.py,sha256=Ti1EdSsrIQMDFTynyoiqC2bi_nHtPZ6uOyj5ERcxiQk,4515
|
|
29
|
+
crossref_local/_core/models.py,sha256=4b8153r7xDBcmTQFsIeSLD1jJm4KvuUkKTLixB7lccM,5562
|
|
30
|
+
crossref_local/_impact_factor/__init__.py,sha256=pcgVCPogBisANYE5Vp2PHVGPgxoMsSXr-6utqVE97-4,559
|
|
31
|
+
crossref_local/_impact_factor/calculator.py,sha256=eZ13URAZzPdRyAQpS8zXe_T33e2lm_gQhtoJCXbfIGM,15977
|
|
32
|
+
crossref_local/_impact_factor/journal_lookup.py,sha256=Ztx6ZeWxfmPvA3KfcW5h_yz01XPstIdk91j3nu2Q-qw,8846
|
|
33
|
+
crossref_local/_remote/__init__.py,sha256=bTA_bed9XsA0HDArm1o0WagsasxN0atzXdRI6I-r7Pk,1412
|
|
34
|
+
crossref_local/_remote/base.py,sha256=gvvvq7uGrG1IOQEZ5ZhWWuakg9Fl_GdkAaU1AfCmtzY,11258
|
|
35
|
+
crossref_local/_remote/collections.py,sha256=840HkakS_AtZbmRMYP-GpUX4S5oJ9jaJXzjivQgyozU,5022
|
|
36
|
+
crossref_local/_server/__init__.py,sha256=4fTk1r__5uWhC6XABVDV7PTPdglFIV32hr9KZ5WbKc8,3869
|
|
37
|
+
crossref_local/_server/middleware.py,sha256=6tdrfFCOA06i0HIDtrHZthY8DXbKy8rMEWHf-3Ve8-Y,926
|
|
38
|
+
crossref_local/_server/models.py,sha256=gjnE-MK4XtfdN_obMyqT5xjNXPa3F-BPrgMrrg908KY,2580
|
|
39
|
+
crossref_local/_server/routes_citations.py,sha256=DUpxl4FdZS7NheM93vCD6ntP5I1Hgw_D8mkGBEMBV2Y,2742
|
|
40
|
+
crossref_local/_server/routes_collections.py,sha256=NaWNTMcswmh87HTZYn5QpzxwtA_c18N3cqy6CrqCBNo,8135
|
|
41
|
+
crossref_local/_server/routes_compat.py,sha256=kSF4_DKg7lcp67Xii95A1pKDBc3kGcqGD-IPaK6TJA8,2726
|
|
42
|
+
crossref_local/_server/routes_works.py,sha256=RxQnkyzoQZtm2n5XTBpZjxjB6ti8GFs-zIzap6VHSgU,3521
|
|
43
|
+
crossref_local/_server/server.py,sha256=D3SSXQ_cBwMqGd9tLwKK84JBDoisvQwVUuu11yr5zhE,579
|
|
44
|
+
crossref_local-0.5.0.dist-info/METADATA,sha256=T2gU3EHRunTfZKRUck9aOrCZAE9HW6VoxjiDdCzyKQs,10748
|
|
45
|
+
crossref_local-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
46
|
+
crossref_local-0.5.0.dist-info/entry_points.txt,sha256=BZbDvHLHzlKzFc-dqLAFwPrWGmGq5yFuD3vslzbmRnk,111
|
|
47
|
+
crossref_local-0.5.0.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
crossref_local/__init__.py,sha256=AJIkriNQBf61SPoi-cfxNr25pdoQMS3KvLhsNoCfJmQ,3316
|
|
2
|
-
crossref_local/__main__.py,sha256=N1c1ESGgJkAwsSWXANUgmzxC1OJEIqw-cl9m4pmNP7s,110
|
|
3
|
-
crossref_local/aio.py,sha256=En2btSn3euRbEYav1919gsmdC8iQaMbgGUso-IThCwo,5490
|
|
4
|
-
crossref_local/api.py,sha256=h2lpoA7qsCzxBTv0Na3Etgk0hZCWZFKmalreIYsmOhw,5343
|
|
5
|
-
crossref_local/citations.py,sha256=QFahv84upNnXP_89A8bHxEbAdz7wHbh5LEniGcAiHas,12402
|
|
6
|
-
crossref_local/cli.py,sha256=WMk7GxJtTf2ZCH1ldkcUmX-643n5DE11kGu_K7AkOrA,15132
|
|
7
|
-
crossref_local/config.py,sha256=4LGZJ3CmsA9YRv48FkEqVR2xljuSjl0MYiMrT8ljk14,5050
|
|
8
|
-
crossref_local/db.py,sha256=x7dXQXjsFN4LavtkNAKTNw1cUBMG-2h53-Z-Xlq6aoQ,3696
|
|
9
|
-
crossref_local/fts.py,sha256=yZMh_vmtFentXKAFGTS4z7ZNNj7p_ItgfFP5i0yQltw,4448
|
|
10
|
-
crossref_local/mcp_server.py,sha256=KDcBvVMXrhemO7cS4kBMfEvp0Qb-LDsVbnPhnLaaC-4,5796
|
|
11
|
-
crossref_local/models.py,sha256=b_yYb91O6RwEPpEqe2Wmdz12WIfE5itjEus4-fCLxLI,5476
|
|
12
|
-
crossref_local/remote.py,sha256=p8P0zotkNYchqqKGOsqcFiHR10qD4pYmJ26-ltyqO4s,8389
|
|
13
|
-
crossref_local/server.py,sha256=lEc0EA3jVx31q1EEYOaT4cr9l2_fGpoQZmpYdnoGxFQ,9034
|
|
14
|
-
crossref_local/impact_factor/__init__.py,sha256=pcgVCPogBisANYE5Vp2PHVGPgxoMsSXr-6utqVE97-4,559
|
|
15
|
-
crossref_local/impact_factor/calculator.py,sha256=eZ13URAZzPdRyAQpS8zXe_T33e2lm_gQhtoJCXbfIGM,15977
|
|
16
|
-
crossref_local/impact_factor/journal_lookup.py,sha256=Ztx6ZeWxfmPvA3KfcW5h_yz01XPstIdk91j3nu2Q-qw,8846
|
|
17
|
-
crossref_local-0.3.1.dist-info/METADATA,sha256=Wiqa6MGJXMgMFI3qXss4IcLr0wo20Jsbl9UgAfyXrrU,8480
|
|
18
|
-
crossref_local-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
-
crossref_local-0.3.1.dist-info/entry_points.txt,sha256=BZbDvHLHzlKzFc-dqLAFwPrWGmGq5yFuD3vslzbmRnk,111
|
|
20
|
-
crossref_local-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|