crossref-local 0.3.1__py3-none-any.whl → 0.4.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 +23 -9
- crossref_local/__main__.py +0 -0
- crossref_local/aio.py +0 -0
- crossref_local/api.py +104 -29
- crossref_local/cache.py +466 -0
- crossref_local/cache_export.py +83 -0
- crossref_local/cache_viz.py +296 -0
- crossref_local/citations.py +0 -0
- crossref_local/cli.py +205 -137
- crossref_local/cli_cache.py +179 -0
- crossref_local/cli_completion.py +245 -0
- crossref_local/cli_main.py +20 -0
- crossref_local/cli_mcp.py +275 -0
- crossref_local/config.py +21 -24
- crossref_local/db.py +0 -0
- crossref_local/fts.py +0 -0
- crossref_local/impact_factor/__init__.py +0 -0
- crossref_local/impact_factor/calculator.py +0 -0
- crossref_local/impact_factor/journal_lookup.py +0 -0
- crossref_local/mcp_server.py +262 -51
- crossref_local/models.py +0 -0
- crossref_local/remote.py +5 -0
- crossref_local/server.py +7 -7
- {crossref_local-0.3.1.dist-info → crossref_local-0.4.0.dist-info}/METADATA +63 -24
- crossref_local-0.4.0.dist-info/RECORD +27 -0
- {crossref_local-0.3.1.dist-info → crossref_local-0.4.0.dist-info}/entry_points.txt +1 -1
- crossref_local-0.3.1.dist-info/RECORD +0 -20
- {crossref_local-0.3.1.dist-info → crossref_local-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Shell completion commands for crossref-local CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
PROG_NAME = "crossref-local"
|
|
10
|
+
|
|
11
|
+
# Shell completion scripts
|
|
12
|
+
BASH_COMPLETION = f"""# {PROG_NAME} bash completion
|
|
13
|
+
eval "$(_CROSSREF_LOCAL_COMPLETE=bash_source {PROG_NAME})"
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
ZSH_COMPLETION = f"""# {PROG_NAME} zsh completion
|
|
17
|
+
eval "$(_CROSSREF_LOCAL_COMPLETE=zsh_source {PROG_NAME})"
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
FISH_COMPLETION = f"""# {PROG_NAME} fish completion
|
|
21
|
+
_CROSSREF_LOCAL_COMPLETE=fish_source {PROG_NAME} | source
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Shell config files
|
|
25
|
+
SHELL_CONFIGS = {
|
|
26
|
+
"bash": [Path.home() / ".bashrc", Path.home() / ".bash_profile"],
|
|
27
|
+
"zsh": [Path.home() / ".zshrc"],
|
|
28
|
+
"fish": [Path.home() / ".config" / "fish" / "config.fish"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
COMPLETION_SCRIPTS = {
|
|
32
|
+
"bash": BASH_COMPLETION,
|
|
33
|
+
"zsh": ZSH_COMPLETION,
|
|
34
|
+
"fish": FISH_COMPLETION,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
COMPLETION_MARKER = f"# >>> {PROG_NAME} completion >>>"
|
|
38
|
+
COMPLETION_END_MARKER = f"# <<< {PROG_NAME} completion <<<"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _detect_shell() -> str:
|
|
42
|
+
"""Detect current shell from $SHELL environment variable."""
|
|
43
|
+
shell_path = os.environ.get("SHELL", "")
|
|
44
|
+
shell_name = Path(shell_path).name if shell_path else ""
|
|
45
|
+
|
|
46
|
+
if shell_name in ("bash", "zsh", "fish"):
|
|
47
|
+
return shell_name
|
|
48
|
+
|
|
49
|
+
# Fallback to bash
|
|
50
|
+
return "bash"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_config_file(shell: str) -> Path | None:
|
|
54
|
+
"""Get the appropriate config file for the shell."""
|
|
55
|
+
configs = SHELL_CONFIGS.get(shell, [])
|
|
56
|
+
for config in configs:
|
|
57
|
+
if config.exists():
|
|
58
|
+
return config
|
|
59
|
+
# Return first option for creation
|
|
60
|
+
return configs[0] if configs else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_installed(shell: str) -> tuple[bool, Path | None]:
|
|
64
|
+
"""Check if completion is already installed for a shell."""
|
|
65
|
+
configs = SHELL_CONFIGS.get(shell, [])
|
|
66
|
+
for config in configs:
|
|
67
|
+
if config.exists():
|
|
68
|
+
content = config.read_text()
|
|
69
|
+
if COMPLETION_MARKER in content:
|
|
70
|
+
return True, config
|
|
71
|
+
return False, None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _install_completion(shell: str) -> tuple[bool, str]:
|
|
75
|
+
"""Install completion for a shell. Returns (success, message)."""
|
|
76
|
+
installed, existing_file = _is_installed(shell)
|
|
77
|
+
if installed:
|
|
78
|
+
return True, f"Already installed in {existing_file}"
|
|
79
|
+
|
|
80
|
+
config_file = _get_config_file(shell)
|
|
81
|
+
if config_file is None:
|
|
82
|
+
return False, f"Could not find config file for {shell}"
|
|
83
|
+
|
|
84
|
+
script = COMPLETION_SCRIPTS.get(shell)
|
|
85
|
+
if script is None:
|
|
86
|
+
return False, f"Unsupported shell: {shell}"
|
|
87
|
+
|
|
88
|
+
# Create parent directory if needed (for fish)
|
|
89
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# Append completion to config file
|
|
92
|
+
completion_block = f"\n{COMPLETION_MARKER}\n{script}{COMPLETION_END_MARKER}\n"
|
|
93
|
+
|
|
94
|
+
with open(config_file, "a") as f:
|
|
95
|
+
f.write(completion_block)
|
|
96
|
+
|
|
97
|
+
return True, f"Installed to {config_file}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _uninstall_completion(shell: str) -> tuple[bool, str]:
|
|
101
|
+
"""Uninstall completion for a shell. Returns (success, message)."""
|
|
102
|
+
installed, config_file = _is_installed(shell)
|
|
103
|
+
if not installed:
|
|
104
|
+
return True, f"Not installed for {shell}"
|
|
105
|
+
|
|
106
|
+
if config_file is None:
|
|
107
|
+
return False, f"Could not find config file for {shell}"
|
|
108
|
+
|
|
109
|
+
content = config_file.read_text()
|
|
110
|
+
|
|
111
|
+
# Remove the completion block
|
|
112
|
+
start_idx = content.find(COMPLETION_MARKER)
|
|
113
|
+
end_idx = content.find(COMPLETION_END_MARKER)
|
|
114
|
+
|
|
115
|
+
if start_idx == -1 or end_idx == -1:
|
|
116
|
+
return False, "Could not find completion block to remove"
|
|
117
|
+
|
|
118
|
+
# Include the newline before marker and after end marker
|
|
119
|
+
if start_idx > 0 and content[start_idx - 1] == "\n":
|
|
120
|
+
start_idx -= 1
|
|
121
|
+
end_idx = end_idx + len(COMPLETION_END_MARKER)
|
|
122
|
+
if end_idx < len(content) and content[end_idx] == "\n":
|
|
123
|
+
end_idx += 1
|
|
124
|
+
|
|
125
|
+
new_content = content[:start_idx] + content[end_idx:]
|
|
126
|
+
config_file.write_text(new_content)
|
|
127
|
+
|
|
128
|
+
return True, f"Removed from {config_file}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@click.group("completion", invoke_without_command=True)
|
|
132
|
+
@click.pass_context
|
|
133
|
+
def completion(ctx):
|
|
134
|
+
"""Shell completion commands.
|
|
135
|
+
|
|
136
|
+
\b
|
|
137
|
+
Install shell tab-completion for crossref-local CLI.
|
|
138
|
+
Running without subcommand auto-installs for detected shell.
|
|
139
|
+
|
|
140
|
+
\b
|
|
141
|
+
Examples:
|
|
142
|
+
crossref-local completion # Auto-install for current shell
|
|
143
|
+
crossref-local completion status # Check installation status
|
|
144
|
+
crossref-local completion bash # Show bash completion script
|
|
145
|
+
"""
|
|
146
|
+
if ctx.invoked_subcommand is None:
|
|
147
|
+
# Auto-install for detected shell
|
|
148
|
+
shell = _detect_shell()
|
|
149
|
+
click.echo(f"Detected shell: {shell}")
|
|
150
|
+
|
|
151
|
+
success, message = _install_completion(shell)
|
|
152
|
+
if success:
|
|
153
|
+
click.echo(f"[OK] {message}")
|
|
154
|
+
click.echo(
|
|
155
|
+
f"\nRestart your shell or run: source ~/{_get_config_file(shell).name}"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
click.echo(f"[ERROR] {message}", err=True)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@completion.command("install")
|
|
163
|
+
@click.option(
|
|
164
|
+
"--shell",
|
|
165
|
+
type=click.Choice(["bash", "zsh", "fish"]),
|
|
166
|
+
default=None,
|
|
167
|
+
help="Shell to install completion for (default: auto-detect)",
|
|
168
|
+
)
|
|
169
|
+
def install_cmd(shell: str | None):
|
|
170
|
+
"""Install completion to shell config file."""
|
|
171
|
+
if shell is None:
|
|
172
|
+
shell = _detect_shell()
|
|
173
|
+
click.echo(f"Detected shell: {shell}")
|
|
174
|
+
|
|
175
|
+
success, message = _install_completion(shell)
|
|
176
|
+
if success:
|
|
177
|
+
click.echo(f"[OK] {message}")
|
|
178
|
+
config_file = _get_config_file(shell)
|
|
179
|
+
if config_file:
|
|
180
|
+
click.echo(f"\nRestart your shell or run: source {config_file}")
|
|
181
|
+
else:
|
|
182
|
+
click.echo(f"[ERROR] {message}", err=True)
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@completion.command("uninstall")
|
|
187
|
+
@click.option(
|
|
188
|
+
"--shell",
|
|
189
|
+
type=click.Choice(["bash", "zsh", "fish"]),
|
|
190
|
+
default=None,
|
|
191
|
+
help="Shell to uninstall completion from (default: auto-detect)",
|
|
192
|
+
)
|
|
193
|
+
def uninstall_cmd(shell: str | None):
|
|
194
|
+
"""Remove completion from shell config file."""
|
|
195
|
+
if shell is None:
|
|
196
|
+
shell = _detect_shell()
|
|
197
|
+
click.echo(f"Detected shell: {shell}")
|
|
198
|
+
|
|
199
|
+
success, message = _uninstall_completion(shell)
|
|
200
|
+
if success:
|
|
201
|
+
click.echo(f"[OK] {message}")
|
|
202
|
+
else:
|
|
203
|
+
click.echo(f"[ERROR] {message}", err=True)
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@completion.command("status")
|
|
208
|
+
def status_cmd():
|
|
209
|
+
"""Check completion installation status for all shells."""
|
|
210
|
+
click.echo(f"{PROG_NAME} Shell Completion Status")
|
|
211
|
+
click.echo("=" * 40)
|
|
212
|
+
|
|
213
|
+
current_shell = _detect_shell()
|
|
214
|
+
click.echo(f"Current shell: {current_shell}")
|
|
215
|
+
click.echo()
|
|
216
|
+
|
|
217
|
+
for shell in ["bash", "zsh", "fish"]:
|
|
218
|
+
installed, config_file = _is_installed(shell)
|
|
219
|
+
marker = "[x]" if installed else "[ ]"
|
|
220
|
+
location = f" ({config_file})" if installed and config_file else ""
|
|
221
|
+
current = " (current)" if shell == current_shell else ""
|
|
222
|
+
click.echo(f" {marker} {shell}{current}{location}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@completion.command("bash")
|
|
226
|
+
def bash_cmd():
|
|
227
|
+
"""Show bash completion script."""
|
|
228
|
+
click.echo(BASH_COMPLETION.strip())
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@completion.command("zsh")
|
|
232
|
+
def zsh_cmd():
|
|
233
|
+
"""Show zsh completion script."""
|
|
234
|
+
click.echo(ZSH_COMPLETION.strip())
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@completion.command("fish")
|
|
238
|
+
def fish_cmd():
|
|
239
|
+
"""Show fish completion script."""
|
|
240
|
+
click.echo(FISH_COMPLETION.strip())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def register_completion_commands(cli_group):
|
|
244
|
+
"""Register completion commands with the main CLI group."""
|
|
245
|
+
cli_group.add_command(completion)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Main CLI entry point with all command groups registered."""
|
|
2
|
+
|
|
3
|
+
from .cli import cli
|
|
4
|
+
from .cli_cache import register_cache_commands
|
|
5
|
+
from .cli_completion import register_completion_commands
|
|
6
|
+
from .cli_mcp import register_mcp_commands
|
|
7
|
+
|
|
8
|
+
# Register command groups
|
|
9
|
+
register_cache_commands(cli)
|
|
10
|
+
register_completion_commands(cli)
|
|
11
|
+
register_mcp_commands(cli)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
"""Entry point for CLI."""
|
|
16
|
+
cli()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""MCP server management commands for crossref-local CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group("mcp", context_settings=CONTEXT_SETTINGS)
|
|
12
|
+
def mcp():
|
|
13
|
+
"""MCP (Model Context Protocol) server management.
|
|
14
|
+
|
|
15
|
+
\b
|
|
16
|
+
Commands for running and managing the MCP server that enables
|
|
17
|
+
AI assistants like Claude to search academic papers.
|
|
18
|
+
|
|
19
|
+
\b
|
|
20
|
+
Quick start:
|
|
21
|
+
crossref-local mcp start # Start stdio server
|
|
22
|
+
crossref-local mcp start -t http # Start HTTP server
|
|
23
|
+
crossref-local mcp doctor # Check dependencies
|
|
24
|
+
crossref-local mcp installation # Show config snippets
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@mcp.command("start", context_settings=CONTEXT_SETTINGS)
|
|
30
|
+
@click.option(
|
|
31
|
+
"-t",
|
|
32
|
+
"--transport",
|
|
33
|
+
type=click.Choice(["stdio", "sse", "http"]),
|
|
34
|
+
default="stdio",
|
|
35
|
+
help="Transport protocol (http recommended for remote)",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--host",
|
|
39
|
+
default="localhost",
|
|
40
|
+
envvar="CROSSREF_LOCAL_MCP_HOST",
|
|
41
|
+
help="Host for HTTP/SSE transport",
|
|
42
|
+
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"--port",
|
|
45
|
+
default=8082,
|
|
46
|
+
type=int,
|
|
47
|
+
envvar="CROSSREF_LOCAL_MCP_PORT",
|
|
48
|
+
help="Port for HTTP/SSE transport",
|
|
49
|
+
)
|
|
50
|
+
def start_cmd(transport: str, host: str, port: int):
|
|
51
|
+
"""Start the MCP server.
|
|
52
|
+
|
|
53
|
+
\b
|
|
54
|
+
Transports:
|
|
55
|
+
stdio - Standard I/O (default, for Claude Desktop local)
|
|
56
|
+
http - Streamable HTTP (recommended for remote/persistent)
|
|
57
|
+
sse - Server-Sent Events (deprecated as of MCP spec 2025-03-26)
|
|
58
|
+
|
|
59
|
+
\b
|
|
60
|
+
Examples:
|
|
61
|
+
crossref-local mcp start # stdio for Claude Desktop
|
|
62
|
+
crossref-local mcp start -t http # HTTP on localhost:8082
|
|
63
|
+
crossref-local mcp start -t http --port 9000 # Custom port
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
from .mcp_server import run_server
|
|
67
|
+
except ImportError:
|
|
68
|
+
click.echo(
|
|
69
|
+
"MCP server requires fastmcp. Install with:\n"
|
|
70
|
+
" pip install crossref-local[mcp]",
|
|
71
|
+
err=True,
|
|
72
|
+
)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
run_server(transport=transport, host=host, port=port)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp.command("doctor", context_settings=CONTEXT_SETTINGS)
|
|
79
|
+
def doctor_cmd():
|
|
80
|
+
"""Check MCP server dependencies and configuration.
|
|
81
|
+
|
|
82
|
+
Verifies that all required packages are installed and
|
|
83
|
+
the database is accessible.
|
|
84
|
+
"""
|
|
85
|
+
click.echo("MCP Server Health Check")
|
|
86
|
+
click.echo("=" * 40)
|
|
87
|
+
|
|
88
|
+
issues = []
|
|
89
|
+
|
|
90
|
+
# Check fastmcp
|
|
91
|
+
try:
|
|
92
|
+
import fastmcp
|
|
93
|
+
|
|
94
|
+
click.echo(f"[OK] fastmcp {fastmcp.__version__}")
|
|
95
|
+
except ImportError:
|
|
96
|
+
click.echo("[FAIL] fastmcp not installed")
|
|
97
|
+
issues.append("Install fastmcp: pip install crossref-local[mcp]")
|
|
98
|
+
|
|
99
|
+
# Check database
|
|
100
|
+
try:
|
|
101
|
+
from . import info
|
|
102
|
+
|
|
103
|
+
db_info = info()
|
|
104
|
+
works = db_info.get("works", 0)
|
|
105
|
+
click.echo(f"[OK] Database: {works:,} works")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
click.echo(f"[FAIL] Database: {e}")
|
|
108
|
+
issues.append("Configure database: export CROSSREF_LOCAL_DB=/path/to/db")
|
|
109
|
+
|
|
110
|
+
# Check FTS index
|
|
111
|
+
try:
|
|
112
|
+
from . import info
|
|
113
|
+
|
|
114
|
+
db_info = info()
|
|
115
|
+
fts = db_info.get("fts_indexed", 0)
|
|
116
|
+
if fts > 0:
|
|
117
|
+
click.echo(f"[OK] FTS index: {fts:,} indexed")
|
|
118
|
+
else:
|
|
119
|
+
click.echo("[WARN] FTS index: not built")
|
|
120
|
+
issues.append("Build FTS index: make fts-build")
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
click.echo()
|
|
125
|
+
if issues:
|
|
126
|
+
click.echo("Issues found:")
|
|
127
|
+
for issue in issues:
|
|
128
|
+
click.echo(f" - {issue}")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
else:
|
|
131
|
+
click.echo("All checks passed!")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp.command("installation", context_settings=CONTEXT_SETTINGS)
|
|
135
|
+
@click.option(
|
|
136
|
+
"-t",
|
|
137
|
+
"--transport",
|
|
138
|
+
type=click.Choice(["stdio", "http"]),
|
|
139
|
+
default="stdio",
|
|
140
|
+
help="Transport type for config",
|
|
141
|
+
)
|
|
142
|
+
@click.option("--host", default="localhost", help="Host for HTTP transport")
|
|
143
|
+
@click.option("--port", default=8082, type=int, help="Port for HTTP transport")
|
|
144
|
+
def installation_cmd(transport: str, host: str, port: int):
|
|
145
|
+
"""Show MCP client configuration snippets.
|
|
146
|
+
|
|
147
|
+
Outputs JSON configuration for Claude Desktop or other MCP clients.
|
|
148
|
+
|
|
149
|
+
\b
|
|
150
|
+
Examples:
|
|
151
|
+
crossref-local mcp installation # stdio config
|
|
152
|
+
crossref-local mcp installation -t http # HTTP config
|
|
153
|
+
"""
|
|
154
|
+
if transport == "stdio":
|
|
155
|
+
config = {
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"crossref-local": {
|
|
158
|
+
"command": "crossref-local",
|
|
159
|
+
"args": ["mcp", "start"],
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
click.echo("Claude Desktop configuration (stdio):")
|
|
164
|
+
click.echo()
|
|
165
|
+
click.echo(
|
|
166
|
+
"Add to ~/Library/Application Support/Claude/claude_desktop_config.json"
|
|
167
|
+
)
|
|
168
|
+
click.echo("or ~/.config/claude/claude_desktop_config.json:")
|
|
169
|
+
click.echo()
|
|
170
|
+
else:
|
|
171
|
+
url = f"http://{host}:{port}/mcp"
|
|
172
|
+
config = {"mcpServers": {"crossref-local": {"url": url}}}
|
|
173
|
+
click.echo(f"Claude Desktop configuration (HTTP at {url}):")
|
|
174
|
+
click.echo()
|
|
175
|
+
click.echo("First start the server:")
|
|
176
|
+
click.echo(f" crossref-local mcp start -t http --host {host} --port {port}")
|
|
177
|
+
click.echo()
|
|
178
|
+
click.echo("Then add to claude_desktop_config.json:")
|
|
179
|
+
click.echo()
|
|
180
|
+
|
|
181
|
+
click.echo(json.dumps(config, indent=2))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@mcp.command("list-tools", context_settings=CONTEXT_SETTINGS)
|
|
185
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
186
|
+
def list_tools_cmd(as_json: bool):
|
|
187
|
+
"""List available MCP tools.
|
|
188
|
+
|
|
189
|
+
Shows all tools exposed by the MCP server with their descriptions.
|
|
190
|
+
"""
|
|
191
|
+
tools = [
|
|
192
|
+
{
|
|
193
|
+
"name": "search",
|
|
194
|
+
"description": "Search for academic works by title, abstract, or authors",
|
|
195
|
+
"parameters": ["query", "limit", "offset", "with_abstracts"],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "search_by_doi",
|
|
199
|
+
"description": "Get detailed information about a work by DOI",
|
|
200
|
+
"parameters": ["doi", "as_citation"],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "status",
|
|
204
|
+
"description": "Get database statistics and status",
|
|
205
|
+
"parameters": [],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "enrich_dois",
|
|
209
|
+
"description": "Enrich DOIs with full metadata including citations",
|
|
210
|
+
"parameters": ["dois"],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"name": "cache_create",
|
|
214
|
+
"description": "Create a paper cache from search query",
|
|
215
|
+
"parameters": ["name", "query", "limit"],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "cache_query",
|
|
219
|
+
"description": "Query cached papers with field filtering",
|
|
220
|
+
"parameters": ["name", "fields", "year_min", "year_max", "limit"],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "cache_stats",
|
|
224
|
+
"description": "Get cache statistics",
|
|
225
|
+
"parameters": ["name"],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "cache_list",
|
|
229
|
+
"description": "List all available caches",
|
|
230
|
+
"parameters": [],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "cache_top_cited",
|
|
234
|
+
"description": "Get top cited papers from cache",
|
|
235
|
+
"parameters": ["name", "n", "year_min", "year_max"],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "cache_citation_summary",
|
|
239
|
+
"description": "Get citation statistics for cached papers",
|
|
240
|
+
"parameters": ["name"],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"name": "cache_plot_scatter",
|
|
244
|
+
"description": "Generate year vs citations scatter plot",
|
|
245
|
+
"parameters": ["name", "output", "top_n"],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "cache_plot_network",
|
|
249
|
+
"description": "Generate citation network visualization",
|
|
250
|
+
"parameters": ["name", "output", "max_nodes"],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "cache_export",
|
|
254
|
+
"description": "Export cache to file (json, csv, bibtex, dois)",
|
|
255
|
+
"parameters": ["name", "output_path", "format", "fields"],
|
|
256
|
+
},
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
if as_json:
|
|
260
|
+
click.echo(json.dumps(tools, indent=2))
|
|
261
|
+
else:
|
|
262
|
+
click.echo("CrossRef Local MCP Tools")
|
|
263
|
+
click.echo("=" * 50)
|
|
264
|
+
click.echo()
|
|
265
|
+
for tool in tools:
|
|
266
|
+
click.echo(f" {tool['name']}")
|
|
267
|
+
click.echo(f" {tool['description']}")
|
|
268
|
+
if tool["parameters"]:
|
|
269
|
+
click.echo(f" Parameters: {', '.join(tool['parameters'])}")
|
|
270
|
+
click.echo()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def register_mcp_commands(cli_group):
|
|
274
|
+
"""Register MCP commands with the main CLI group."""
|
|
275
|
+
cli_group.add_command(mcp)
|
crossref_local/config.py
CHANGED
|
@@ -6,16 +6,13 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
# Default database locations (checked in order)
|
|
8
8
|
DEFAULT_DB_PATHS = [
|
|
9
|
-
Path("/home/ywatanabe/proj/crossref-local/data/crossref.db"),
|
|
10
|
-
Path("/home/ywatanabe/proj/crossref_local/data/crossref.db"),
|
|
11
|
-
Path("/mnt/nas_ug/crossref_local/data/crossref.db"),
|
|
12
|
-
Path.home() / ".crossref_local" / "crossref.db",
|
|
13
9
|
Path.cwd() / "data" / "crossref.db",
|
|
10
|
+
Path.home() / ".crossref_local" / "crossref.db",
|
|
14
11
|
]
|
|
15
12
|
|
|
16
13
|
# Default remote API URL (via SSH tunnel)
|
|
17
14
|
DEFAULT_API_URLS = [
|
|
18
|
-
"http://localhost:
|
|
15
|
+
"http://localhost:8333", # SSH tunnel to NAS
|
|
19
16
|
]
|
|
20
17
|
DEFAULT_API_URL = DEFAULT_API_URLS[0]
|
|
21
18
|
|
|
@@ -58,7 +55,7 @@ class Config:
|
|
|
58
55
|
|
|
59
56
|
_db_path: Optional[Path] = None
|
|
60
57
|
_api_url: Optional[str] = None
|
|
61
|
-
_mode: str = "auto" # "auto", "
|
|
58
|
+
_mode: str = "auto" # "auto", "db", or "http"
|
|
62
59
|
|
|
63
60
|
@classmethod
|
|
64
61
|
def get_mode(cls) -> str:
|
|
@@ -66,36 +63,36 @@ class Config:
|
|
|
66
63
|
Get current mode.
|
|
67
64
|
|
|
68
65
|
Returns:
|
|
69
|
-
"
|
|
70
|
-
"
|
|
66
|
+
"db" if using direct database access
|
|
67
|
+
"http" if using HTTP API
|
|
71
68
|
"""
|
|
72
69
|
if cls._mode == "auto":
|
|
73
70
|
# Check environment variable
|
|
74
71
|
env_mode = os.environ.get("CROSSREF_LOCAL_MODE", "").lower()
|
|
75
|
-
if env_mode in ("remote", "api"):
|
|
76
|
-
return "
|
|
77
|
-
if env_mode
|
|
78
|
-
return "
|
|
72
|
+
if env_mode in ("http", "remote", "api"):
|
|
73
|
+
return "http"
|
|
74
|
+
if env_mode in ("db", "local"):
|
|
75
|
+
return "db"
|
|
79
76
|
|
|
80
77
|
# Check if API URL is set
|
|
81
|
-
if cls._api_url or os.environ.get("
|
|
82
|
-
return "
|
|
78
|
+
if cls._api_url or os.environ.get("CROSSREF_LOCAL_API_URL"):
|
|
79
|
+
return "http"
|
|
83
80
|
|
|
84
81
|
# Check if local database exists
|
|
85
82
|
try:
|
|
86
83
|
get_db_path()
|
|
87
|
-
return "
|
|
84
|
+
return "db"
|
|
88
85
|
except FileNotFoundError:
|
|
89
|
-
# No local DB, try
|
|
90
|
-
return "
|
|
86
|
+
# No local DB, try http
|
|
87
|
+
return "http"
|
|
91
88
|
|
|
92
89
|
return cls._mode
|
|
93
90
|
|
|
94
91
|
@classmethod
|
|
95
92
|
def set_mode(cls, mode: str) -> None:
|
|
96
|
-
"""Set mode explicitly: '
|
|
97
|
-
if mode not in ("auto", "
|
|
98
|
-
raise ValueError(f"Invalid mode: {mode}. Use 'auto', '
|
|
93
|
+
"""Set mode explicitly: 'db', 'http', or 'auto'."""
|
|
94
|
+
if mode not in ("auto", "db", "http"):
|
|
95
|
+
raise ValueError(f"Invalid mode: {mode}. Use 'auto', 'db', or 'http'")
|
|
99
96
|
cls._mode = mode
|
|
100
97
|
|
|
101
98
|
@classmethod
|
|
@@ -112,7 +109,7 @@ class Config:
|
|
|
112
109
|
if not path.exists():
|
|
113
110
|
raise FileNotFoundError(f"Database not found: {path}")
|
|
114
111
|
cls._db_path = path
|
|
115
|
-
cls._mode = "
|
|
112
|
+
cls._mode = "db"
|
|
116
113
|
|
|
117
114
|
@classmethod
|
|
118
115
|
def get_api_url(cls, auto_detect: bool = True) -> str:
|
|
@@ -128,7 +125,7 @@ class Config:
|
|
|
128
125
|
if cls._api_url:
|
|
129
126
|
return cls._api_url
|
|
130
127
|
|
|
131
|
-
env_url = os.environ.get("
|
|
128
|
+
env_url = os.environ.get("CROSSREF_LOCAL_API_URL")
|
|
132
129
|
if env_url:
|
|
133
130
|
return env_url
|
|
134
131
|
|
|
@@ -159,9 +156,9 @@ class Config:
|
|
|
159
156
|
|
|
160
157
|
@classmethod
|
|
161
158
|
def set_api_url(cls, url: str) -> None:
|
|
162
|
-
"""Set API URL for
|
|
159
|
+
"""Set API URL for http mode."""
|
|
163
160
|
cls._api_url = url.rstrip("/")
|
|
164
|
-
cls._mode = "
|
|
161
|
+
cls._mode = "http"
|
|
165
162
|
|
|
166
163
|
@classmethod
|
|
167
164
|
def reset(cls) -> None:
|
crossref_local/db.py
CHANGED
|
File without changes
|
crossref_local/fts.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|