crossref-local 0.4.0__py3-none-any.whl → 0.5.1__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 +24 -10
- 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 → _cache/export.py} +27 -10
- crossref_local/_cache/utils.py +93 -0
- crossref_local/_cli/__init__.py +9 -0
- crossref_local/_cli/cli.py +389 -0
- crossref_local/_cli/mcp.py +351 -0
- crossref_local/_cli/mcp_server.py +457 -0
- crossref_local/_cli/search.py +199 -0
- crossref_local/_core/__init__.py +62 -0
- crossref_local/{api.py → _core/api.py} +26 -5
- crossref_local/{citations.py → _core/citations.py} +55 -26
- crossref_local/{config.py → _core/config.py} +40 -22
- crossref_local/{db.py → _core/db.py} +32 -26
- crossref_local/_core/export.py +344 -0
- crossref_local/{fts.py → _core/fts.py} +37 -14
- crossref_local/{models.py → _core/models.py} +120 -6
- crossref_local/_remote/__init__.py +56 -0
- crossref_local/_remote/base.py +378 -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 +143 -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 +178 -0
- crossref_local/_server/server.py +19 -0
- crossref_local/aio.py +30 -206
- crossref_local/cache.py +100 -100
- crossref_local/cli.py +5 -515
- crossref_local/jobs.py +169 -0
- crossref_local/mcp_server.py +5 -410
- crossref_local/remote.py +5 -266
- crossref_local/server.py +5 -349
- {crossref_local-0.4.0.dist-info → crossref_local-0.5.1.dist-info}/METADATA +36 -11
- crossref_local-0.5.1.dist-info/RECORD +49 -0
- {crossref_local-0.4.0.dist-info → crossref_local-0.5.1.dist-info}/entry_points.txt +1 -1
- crossref_local/cli_mcp.py +0 -275
- crossref_local-0.4.0.dist-info/RECORD +0 -27
- /crossref_local/{cache_viz.py → _cache/viz.py} +0 -0
- /crossref_local/{cli_cache.py → _cli/cache.py} +0 -0
- /crossref_local/{cli_completion.py → _cli/completion.py} +0 -0
- /crossref_local/{cli_main.py → _cli/main.py} +0 -0
- /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-0.4.0.dist-info → crossref_local-0.5.1.dist-info}/WHEEL +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 8333
|
|
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 8333
|
|
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, __version__
|
|
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=__version__,
|
|
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 = __version__
|
|
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": __version__,
|
|
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 = 8333):
|
|
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.1
|
|
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'
|
|
@@ -53,7 +54,10 @@ Description-Content-Type: text/markdown
|
|
|
53
54
|
|
|
54
55
|
Local CrossRef database with 167M+ scholarly works, full-text search, and impact factor calculation.
|
|
55
56
|
|
|
57
|
+
[](https://badge.fury.io/py/crossref-local)
|
|
58
|
+
[](https://crossref-local.readthedocs.io/en/latest/)
|
|
56
59
|
[](https://github.com/ywatanabe1989/crossref-local/actions/workflows/test.yml)
|
|
60
|
+
[](https://codecov.io/gh/ywatanabe1989/crossref-local)
|
|
57
61
|
[](https://www.python.org/downloads/)
|
|
58
62
|
[](LICENSE)
|
|
59
63
|
|
|
@@ -181,34 +185,46 @@ Found 4 matches in 128.4ms
|
|
|
181
185
|
|
|
182
186
|
Start the FastAPI server:
|
|
183
187
|
```bash
|
|
184
|
-
crossref-local
|
|
188
|
+
crossref-local relay --host 0.0.0.0 --port 31291
|
|
185
189
|
```
|
|
186
190
|
|
|
187
191
|
Endpoints:
|
|
188
192
|
```bash
|
|
189
193
|
# Search works (FTS5)
|
|
190
|
-
curl "http://localhost:
|
|
194
|
+
curl "http://localhost:31291/works?q=CRISPR&limit=10"
|
|
191
195
|
|
|
192
196
|
# Get by DOI
|
|
193
|
-
curl "http://localhost:
|
|
197
|
+
curl "http://localhost:31291/works/10.1038/nature12373"
|
|
194
198
|
|
|
195
199
|
# Batch DOI lookup
|
|
196
|
-
curl -X POST "http://localhost:
|
|
200
|
+
curl -X POST "http://localhost:31291/works/batch" \
|
|
197
201
|
-H "Content-Type: application/json" \
|
|
198
202
|
-d '{"dois": ["10.1038/nature12373", "10.1126/science.aax0758"]}'
|
|
199
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
|
+
|
|
200
216
|
# Database info
|
|
201
|
-
curl "http://localhost:
|
|
217
|
+
curl "http://localhost:31291/info"
|
|
202
218
|
```
|
|
203
219
|
|
|
204
220
|
HTTP mode (connect to running server):
|
|
205
221
|
```bash
|
|
206
222
|
# On local machine (if server is remote)
|
|
207
|
-
ssh -L
|
|
223
|
+
ssh -L 31291:127.0.0.1:31291 your-server
|
|
208
224
|
|
|
209
225
|
# Python client
|
|
210
226
|
from crossref_local import configure_http
|
|
211
|
-
configure_http("http://localhost:
|
|
227
|
+
configure_http("http://localhost:31291")
|
|
212
228
|
|
|
213
229
|
# Or via CLI
|
|
214
230
|
crossref-local --http search "CRISPR"
|
|
@@ -221,7 +237,7 @@ crossref-local --http search "CRISPR"
|
|
|
221
237
|
|
|
222
238
|
Run as MCP (Model Context Protocol) server:
|
|
223
239
|
```bash
|
|
224
|
-
crossref-local
|
|
240
|
+
crossref-local mcp start
|
|
225
241
|
```
|
|
226
242
|
|
|
227
243
|
Local MCP client configuration:
|
|
@@ -230,7 +246,7 @@ Local MCP client configuration:
|
|
|
230
246
|
"mcpServers": {
|
|
231
247
|
"crossref-local": {
|
|
232
248
|
"command": "crossref-local",
|
|
233
|
-
"args": ["
|
|
249
|
+
"args": ["mcp", "start"],
|
|
234
250
|
"env": {
|
|
235
251
|
"CROSSREF_LOCAL_DB": "/path/to/crossref.db"
|
|
236
252
|
}
|
|
@@ -242,7 +258,7 @@ Local MCP client configuration:
|
|
|
242
258
|
Remote MCP via HTTP (recommended):
|
|
243
259
|
```bash
|
|
244
260
|
# On server: start persistent MCP server
|
|
245
|
-
crossref-local
|
|
261
|
+
crossref-local mcp start -t http --host 0.0.0.0 --port 8082
|
|
246
262
|
```
|
|
247
263
|
```json
|
|
248
264
|
{
|
|
@@ -254,12 +270,21 @@ crossref-local run-server-mcp -t http --host 0.0.0.0 --port 8082
|
|
|
254
270
|
}
|
|
255
271
|
```
|
|
256
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
|
+
|
|
257
280
|
See [docs/remote-deployment.md](docs/remote-deployment.md) for systemd and Docker setup.
|
|
258
281
|
|
|
259
282
|
Available tools:
|
|
260
283
|
- `search` - Full-text search across 167M+ papers
|
|
261
284
|
- `search_by_doi` - Get paper by DOI
|
|
285
|
+
- `enrich_dois` - Add citation counts and references to DOIs
|
|
262
286
|
- `status` - Database statistics
|
|
287
|
+
- `cache_*` - Paper collection management
|
|
263
288
|
|
|
264
289
|
</details>
|
|
265
290
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
crossref_local/__init__.py,sha256=j2HyOmjYFUjiywoLdIgYVqgXuGx9eWjcy4x7VBA4vf0,3792
|
|
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=TFhj7BjSHxs3rWyOifpi39z6-YmHIHt5Jz-cZNtVtEg,12425
|
|
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=mplCUBph9WM_Eq7b-MXgT-vvUH9hf4h_eeHUHTQ7f5c,12526
|
|
23
|
+
crossref_local/_cli/search.py,sha256=bqsWn4nCCW_Glgb7dSXsntP4P5nHQJJCSgehvnV9Bbc,6327
|
|
24
|
+
crossref_local/_core/__init__.py,sha256=xfun82MI9-SuaBqX0zB5liiLmj3AkY5TlksVYY6IaOg,1034
|
|
25
|
+
crossref_local/_core/api.py,sha256=fYkr2VkiAQur3fdysIXP57qk0njWBf9ngg6dt5t63Do,7880
|
|
26
|
+
crossref_local/_core/citations.py,sha256=4ERCTVkZu6kesZRazaWcTpUUeYz0rs97NwFZAYcLdKA,12765
|
|
27
|
+
crossref_local/_core/config.py,sha256=T-2EX4EDoCLaOAmViBKGQuNvoJP8x7cvZOvmAcNrQms,5475
|
|
28
|
+
crossref_local/_core/db.py,sha256=KLlcdNWLnKSexc0cuRZGsBLmJUnXeEaD_CBcXhbPXLg,3832
|
|
29
|
+
crossref_local/_core/export.py,sha256=uM_Qb9WXmn7v4tDOQjBlhJNQY0Y0WbefHxNLrRhe5tI,8899
|
|
30
|
+
crossref_local/_core/fts.py,sha256=Jsj3Cm9pihICk7Rvr9vIZbUsoxKzL0OligFFQxtQDVA,5030
|
|
31
|
+
crossref_local/_core/models.py,sha256=tbo0hIViKM9eJQXzp2APFhRvYd1UusUdJHugpPyNAEo,8697
|
|
32
|
+
crossref_local/_impact_factor/__init__.py,sha256=pcgVCPogBisANYE5Vp2PHVGPgxoMsSXr-6utqVE97-4,559
|
|
33
|
+
crossref_local/_impact_factor/calculator.py,sha256=eZ13URAZzPdRyAQpS8zXe_T33e2lm_gQhtoJCXbfIGM,15977
|
|
34
|
+
crossref_local/_impact_factor/journal_lookup.py,sha256=Ztx6ZeWxfmPvA3KfcW5h_yz01XPstIdk91j3nu2Q-qw,8846
|
|
35
|
+
crossref_local/_remote/__init__.py,sha256=bTA_bed9XsA0HDArm1o0WagsasxN0atzXdRI6I-r7Pk,1412
|
|
36
|
+
crossref_local/_remote/base.py,sha256=VlQCPdhRcUynB1IAnJAyfXhxzfsLa0tOSSINAH8lMuI,12160
|
|
37
|
+
crossref_local/_remote/collections.py,sha256=840HkakS_AtZbmRMYP-GpUX4S5oJ9jaJXzjivQgyozU,5022
|
|
38
|
+
crossref_local/_server/__init__.py,sha256=4fTk1r__5uWhC6XABVDV7PTPdglFIV32hr9KZ5WbKc8,3869
|
|
39
|
+
crossref_local/_server/middleware.py,sha256=6tdrfFCOA06i0HIDtrHZthY8DXbKy8rMEWHf-3Ve8-Y,926
|
|
40
|
+
crossref_local/_server/models.py,sha256=8ylDo3UhNoRGjXg5bmiUvB-BWy_8nahD-PSQSEK_8wk,2965
|
|
41
|
+
crossref_local/_server/routes_citations.py,sha256=DUpxl4FdZS7NheM93vCD6ntP5I1Hgw_D8mkGBEMBV2Y,2742
|
|
42
|
+
crossref_local/_server/routes_collections.py,sha256=NaWNTMcswmh87HTZYn5QpzxwtA_c18N3cqy6CrqCBNo,8135
|
|
43
|
+
crossref_local/_server/routes_compat.py,sha256=kSF4_DKg7lcp67Xii95A1pKDBc3kGcqGD-IPaK6TJA8,2726
|
|
44
|
+
crossref_local/_server/routes_works.py,sha256=YtDKh-xxTCvbDf8K7kwxWszIYjYz-PZzqJH8q13CoFE,5080
|
|
45
|
+
crossref_local/_server/server.py,sha256=D3SSXQ_cBwMqGd9tLwKK84JBDoisvQwVUuu11yr5zhE,579
|
|
46
|
+
crossref_local-0.5.1.dist-info/METADATA,sha256=RXoY2iFOTFVQcDQq0vPIwBpqz1rYPIiA4Uz-kj0-YZM,10748
|
|
47
|
+
crossref_local-0.5.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
crossref_local-0.5.1.dist-info/entry_points.txt,sha256=BZbDvHLHzlKzFc-dqLAFwPrWGmGq5yFuD3vslzbmRnk,111
|
|
49
|
+
crossref_local-0.5.1.dist-info/RECORD,,
|