flonat-research 0.1.0

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.
Files changed (285) hide show
  1. package/.claude/agents/domain-reviewer.md +336 -0
  2. package/.claude/agents/fixer.md +226 -0
  3. package/.claude/agents/paper-critic.md +370 -0
  4. package/.claude/agents/peer-reviewer.md +289 -0
  5. package/.claude/agents/proposal-reviewer.md +215 -0
  6. package/.claude/agents/referee2-reviewer.md +367 -0
  7. package/.claude/agents/references/journal-referee-profiles.md +354 -0
  8. package/.claude/agents/references/paper-critic/council-personas.md +77 -0
  9. package/.claude/agents/references/paper-critic/council-prompts.md +198 -0
  10. package/.claude/agents/references/peer-reviewer/report-template.md +199 -0
  11. package/.claude/agents/references/peer-reviewer/sa-prompts.md +260 -0
  12. package/.claude/agents/references/peer-reviewer/security-scan.md +188 -0
  13. package/.claude/agents/references/proposal-reviewer/report-template.md +144 -0
  14. package/.claude/agents/references/proposal-reviewer/sa-prompts.md +149 -0
  15. package/.claude/agents/references/referee-config.md +114 -0
  16. package/.claude/agents/references/referee2-reviewer/audit-checklists.md +287 -0
  17. package/.claude/agents/references/referee2-reviewer/report-template.md +334 -0
  18. package/.claude/rules/design-before-results.md +52 -0
  19. package/.claude/rules/ignore-agents-md.md +17 -0
  20. package/.claude/rules/ignore-gemini-md.md +17 -0
  21. package/.claude/rules/lean-claude-md.md +45 -0
  22. package/.claude/rules/learn-tags.md +99 -0
  23. package/.claude/rules/overleaf-separation.md +67 -0
  24. package/.claude/rules/plan-first.md +175 -0
  25. package/.claude/rules/read-docs-first.md +50 -0
  26. package/.claude/rules/scope-discipline.md +28 -0
  27. package/.claude/settings.json +125 -0
  28. package/.context/current-focus.md +33 -0
  29. package/.context/preferences/priorities.md +36 -0
  30. package/.context/preferences/task-naming.md +28 -0
  31. package/.context/profile.md +29 -0
  32. package/.context/projects/_index.md +41 -0
  33. package/.context/projects/papers/nudge-exp.md +22 -0
  34. package/.context/projects/papers/uncertainty.md +31 -0
  35. package/.context/resources/claude-scientific-writer-review.md +48 -0
  36. package/.context/resources/cunningham-multi-analyst-agents.md +104 -0
  37. package/.context/resources/cunningham-multilang-code-audit.md +62 -0
  38. package/.context/resources/google-ai-co-scientist-review.md +72 -0
  39. package/.context/resources/karpathy-llm-council-review.md +58 -0
  40. package/.context/resources/multi-coder-reliability-protocol.md +175 -0
  41. package/.context/resources/pedro-santanna-takeaways.md +96 -0
  42. package/.context/resources/venue-rankings/abs_ajg_2024.csv +1823 -0
  43. package/.context/resources/venue-rankings/abs_ajg_2024_econ.csv +356 -0
  44. package/.context/resources/venue-rankings/cabs_4_4star_theory.csv +40 -0
  45. package/.context/resources/venue-rankings/core_2026.csv +801 -0
  46. package/.context/resources/venue-rankings.md +147 -0
  47. package/.context/workflows/README.md +69 -0
  48. package/.context/workflows/daily-review.md +91 -0
  49. package/.context/workflows/meeting-actions.md +108 -0
  50. package/.context/workflows/replication-protocol.md +155 -0
  51. package/.context/workflows/weekly-review.md +113 -0
  52. package/.mcp-server-biblio/formatters.py +158 -0
  53. package/.mcp-server-biblio/pyproject.toml +11 -0
  54. package/.mcp-server-biblio/server.py +678 -0
  55. package/.mcp-server-biblio/sources/__init__.py +14 -0
  56. package/.mcp-server-biblio/sources/base.py +73 -0
  57. package/.mcp-server-biblio/sources/formatters.py +83 -0
  58. package/.mcp-server-biblio/sources/models.py +22 -0
  59. package/.mcp-server-biblio/sources/multi_source.py +243 -0
  60. package/.mcp-server-biblio/sources/openalex_source.py +183 -0
  61. package/.mcp-server-biblio/sources/scopus_source.py +309 -0
  62. package/.mcp-server-biblio/sources/wos_source.py +508 -0
  63. package/.mcp-server-biblio/uv.lock +896 -0
  64. package/.scripts/README.md +161 -0
  65. package/.scripts/ai_pattern_density.py +446 -0
  66. package/.scripts/conf +445 -0
  67. package/.scripts/config.py +122 -0
  68. package/.scripts/count_inventory.py +275 -0
  69. package/.scripts/daily_digest.py +288 -0
  70. package/.scripts/done +177 -0
  71. package/.scripts/extract_meeting_actions.py +223 -0
  72. package/.scripts/focus +176 -0
  73. package/.scripts/generate-codex-agents-md.py +217 -0
  74. package/.scripts/inbox +194 -0
  75. package/.scripts/notion_helpers.py +325 -0
  76. package/.scripts/openalex/query_helpers.py +306 -0
  77. package/.scripts/papers +227 -0
  78. package/.scripts/query +223 -0
  79. package/.scripts/session-history.py +201 -0
  80. package/.scripts/skill-health.py +516 -0
  81. package/.scripts/skill-log-miner.py +273 -0
  82. package/.scripts/sync-to-codex.sh +252 -0
  83. package/.scripts/task +213 -0
  84. package/.scripts/tasks +190 -0
  85. package/.scripts/week +206 -0
  86. package/CLAUDE.md +197 -0
  87. package/LICENSE +21 -0
  88. package/MEMORY.md +38 -0
  89. package/README.md +269 -0
  90. package/docs/agents.md +44 -0
  91. package/docs/bibliography-setup.md +55 -0
  92. package/docs/council-mode.md +36 -0
  93. package/docs/getting-started.md +245 -0
  94. package/docs/hooks.md +38 -0
  95. package/docs/mcp-servers.md +82 -0
  96. package/docs/notion-setup.md +109 -0
  97. package/docs/rules.md +33 -0
  98. package/docs/scripts.md +303 -0
  99. package/docs/setup-overview/setup-overview.pdf +0 -0
  100. package/docs/skills.md +70 -0
  101. package/docs/system.md +159 -0
  102. package/hooks/block-destructive-git.sh +66 -0
  103. package/hooks/context-monitor.py +114 -0
  104. package/hooks/postcompact-restore.py +157 -0
  105. package/hooks/precompact-autosave.py +181 -0
  106. package/hooks/promise-checker.sh +124 -0
  107. package/hooks/protect-source-files.sh +81 -0
  108. package/hooks/resume-context-loader.sh +53 -0
  109. package/hooks/startup-context-loader.sh +102 -0
  110. package/package.json +51 -0
  111. package/packages/cli-council/.github/workflows/claude-code-review.yml +44 -0
  112. package/packages/cli-council/.github/workflows/claude.yml +50 -0
  113. package/packages/cli-council/README.md +100 -0
  114. package/packages/cli-council/pyproject.toml +43 -0
  115. package/packages/cli-council/src/cli_council/__init__.py +19 -0
  116. package/packages/cli-council/src/cli_council/__main__.py +185 -0
  117. package/packages/cli-council/src/cli_council/backends/__init__.py +8 -0
  118. package/packages/cli-council/src/cli_council/backends/base.py +81 -0
  119. package/packages/cli-council/src/cli_council/backends/claude.py +25 -0
  120. package/packages/cli-council/src/cli_council/backends/codex.py +27 -0
  121. package/packages/cli-council/src/cli_council/backends/gemini.py +26 -0
  122. package/packages/cli-council/src/cli_council/checkpoint.py +212 -0
  123. package/packages/cli-council/src/cli_council/config.py +51 -0
  124. package/packages/cli-council/src/cli_council/council.py +391 -0
  125. package/packages/cli-council/src/cli_council/models.py +46 -0
  126. package/packages/llm-council/.github/workflows/claude-code-review.yml +44 -0
  127. package/packages/llm-council/.github/workflows/claude.yml +50 -0
  128. package/packages/llm-council/README.md +453 -0
  129. package/packages/llm-council/pyproject.toml +42 -0
  130. package/packages/llm-council/src/llm_council/__init__.py +23 -0
  131. package/packages/llm-council/src/llm_council/__main__.py +259 -0
  132. package/packages/llm-council/src/llm_council/checkpoint.py +193 -0
  133. package/packages/llm-council/src/llm_council/client.py +253 -0
  134. package/packages/llm-council/src/llm_council/config.py +232 -0
  135. package/packages/llm-council/src/llm_council/council.py +482 -0
  136. package/packages/llm-council/src/llm_council/models.py +46 -0
  137. package/packages/mcp-bibliography/MEMORY.md +31 -0
  138. package/packages/mcp-bibliography/_app.py +226 -0
  139. package/packages/mcp-bibliography/formatters.py +158 -0
  140. package/packages/mcp-bibliography/log/2026-03-13-2100.md +35 -0
  141. package/packages/mcp-bibliography/pyproject.toml +15 -0
  142. package/packages/mcp-bibliography/run.sh +20 -0
  143. package/packages/mcp-bibliography/scholarly_formatters.py +83 -0
  144. package/packages/mcp-bibliography/server.py +1857 -0
  145. package/packages/mcp-bibliography/tools/__init__.py +28 -0
  146. package/packages/mcp-bibliography/tools/_registry.py +19 -0
  147. package/packages/mcp-bibliography/tools/altmetric.py +107 -0
  148. package/packages/mcp-bibliography/tools/core.py +92 -0
  149. package/packages/mcp-bibliography/tools/dblp.py +52 -0
  150. package/packages/mcp-bibliography/tools/openalex.py +296 -0
  151. package/packages/mcp-bibliography/tools/opencitations.py +102 -0
  152. package/packages/mcp-bibliography/tools/openreview.py +179 -0
  153. package/packages/mcp-bibliography/tools/orcid.py +131 -0
  154. package/packages/mcp-bibliography/tools/scholarly.py +575 -0
  155. package/packages/mcp-bibliography/tools/unpaywall.py +63 -0
  156. package/packages/mcp-bibliography/tools/zenodo.py +123 -0
  157. package/packages/mcp-bibliography/uv.lock +711 -0
  158. package/scripts/setup.sh +143 -0
  159. package/skills/beamer-deck/SKILL.md +199 -0
  160. package/skills/beamer-deck/references/quality-rubric.md +54 -0
  161. package/skills/beamer-deck/references/review-prompts.md +106 -0
  162. package/skills/bib-validate/SKILL.md +261 -0
  163. package/skills/bib-validate/references/council-mode.md +34 -0
  164. package/skills/bib-validate/references/deep-verify.md +79 -0
  165. package/skills/bib-validate/references/fix-mode.md +36 -0
  166. package/skills/bib-validate/references/openalex-verification.md +45 -0
  167. package/skills/bib-validate/references/preprint-check.md +31 -0
  168. package/skills/bib-validate/references/ref-manager-crossref.md +41 -0
  169. package/skills/bib-validate/references/report-template.md +82 -0
  170. package/skills/code-archaeology/SKILL.md +141 -0
  171. package/skills/code-review/SKILL.md +265 -0
  172. package/skills/code-review/references/quality-rubric.md +67 -0
  173. package/skills/consolidate-memory/SKILL.md +208 -0
  174. package/skills/context-status/SKILL.md +126 -0
  175. package/skills/creation-guard/SKILL.md +230 -0
  176. package/skills/devils-advocate/SKILL.md +130 -0
  177. package/skills/devils-advocate/references/competing-hypotheses.md +83 -0
  178. package/skills/init-project/SKILL.md +115 -0
  179. package/skills/init-project-course/references/memory-and-settings.md +92 -0
  180. package/skills/init-project-course/references/organise-templates.md +94 -0
  181. package/skills/init-project-course/skill.md +147 -0
  182. package/skills/init-project-light/skill.md +139 -0
  183. package/skills/init-project-research/SKILL.md +368 -0
  184. package/skills/init-project-research/references/atlas-pipeline-sync.md +70 -0
  185. package/skills/init-project-research/references/atlas-schema.md +81 -0
  186. package/skills/init-project-research/references/confirmation-report.md +39 -0
  187. package/skills/init-project-research/references/domain-profile-template.md +104 -0
  188. package/skills/init-project-research/references/interview-round3.md +34 -0
  189. package/skills/init-project-research/references/literature-discovery.md +43 -0
  190. package/skills/init-project-research/references/scaffold-details.md +197 -0
  191. package/skills/init-project-research/templates/field-calibration.md +60 -0
  192. package/skills/init-project-research/templates/pipeline-manifest.md +63 -0
  193. package/skills/init-project-research/templates/run-all.sh +116 -0
  194. package/skills/init-project-research/templates/seed-files.md +337 -0
  195. package/skills/insights-deck/SKILL.md +151 -0
  196. package/skills/interview-me/SKILL.md +157 -0
  197. package/skills/latex/SKILL.md +141 -0
  198. package/skills/latex/references/latex-configs.md +183 -0
  199. package/skills/latex-autofix/SKILL.md +230 -0
  200. package/skills/latex-autofix/references/known-errors.md +183 -0
  201. package/skills/latex-autofix/references/quality-rubric.md +50 -0
  202. package/skills/latex-health-check/SKILL.md +161 -0
  203. package/skills/learn/SKILL.md +220 -0
  204. package/skills/learn/scripts/validate_skill.py +265 -0
  205. package/skills/lessons-learned/SKILL.md +201 -0
  206. package/skills/literature/SKILL.md +335 -0
  207. package/skills/literature/references/agent-templates.md +393 -0
  208. package/skills/literature/references/bibliometric-apis.md +44 -0
  209. package/skills/literature/references/cli-council-search.md +79 -0
  210. package/skills/literature/references/openalex-api-guide.md +371 -0
  211. package/skills/literature/references/openalex-common-queries.md +381 -0
  212. package/skills/literature/references/openalex-workflows.md +248 -0
  213. package/skills/literature/references/reference-manager-sync.md +36 -0
  214. package/skills/literature/references/scopus-api-guide.md +208 -0
  215. package/skills/literature/references/wos-api-guide.md +308 -0
  216. package/skills/multi-perspective/SKILL.md +311 -0
  217. package/skills/multi-perspective/references/computational-many-analysts.md +77 -0
  218. package/skills/pipeline-manifest/SKILL.md +226 -0
  219. package/skills/pre-submission-report/SKILL.md +153 -0
  220. package/skills/process-reviews/SKILL.md +244 -0
  221. package/skills/process-reviews/references/rr-routing.md +101 -0
  222. package/skills/project-deck/SKILL.md +87 -0
  223. package/skills/project-safety/SKILL.md +135 -0
  224. package/skills/proofread/SKILL.md +254 -0
  225. package/skills/proofread/references/quality-rubric.md +104 -0
  226. package/skills/python-env/SKILL.md +57 -0
  227. package/skills/quarto-deck/SKILL.md +226 -0
  228. package/skills/quarto-deck/references/markdown-format.md +143 -0
  229. package/skills/quarto-deck/references/quality-rubric.md +54 -0
  230. package/skills/save-context/SKILL.md +174 -0
  231. package/skills/session-log/SKILL.md +98 -0
  232. package/skills/shared/concept-validation-gate.md +161 -0
  233. package/skills/shared/council-protocol.md +265 -0
  234. package/skills/shared/distribution-diagnostics.md +164 -0
  235. package/skills/shared/engagement-stratified-sampling.md +218 -0
  236. package/skills/shared/escalation-protocol.md +74 -0
  237. package/skills/shared/external-audit-protocol.md +205 -0
  238. package/skills/shared/intercoder-reliability.md +256 -0
  239. package/skills/shared/mcp-degradation.md +81 -0
  240. package/skills/shared/method-probing-questions.md +163 -0
  241. package/skills/shared/multi-language-conventions.md +143 -0
  242. package/skills/shared/paid-api-safety.md +174 -0
  243. package/skills/shared/palettes.md +90 -0
  244. package/skills/shared/progressive-disclosure.md +92 -0
  245. package/skills/shared/project-documentation-content.md +443 -0
  246. package/skills/shared/project-documentation-format.md +281 -0
  247. package/skills/shared/project-documentation.md +100 -0
  248. package/skills/shared/publication-output.md +138 -0
  249. package/skills/shared/quality-scoring.md +70 -0
  250. package/skills/shared/reference-resolution.md +77 -0
  251. package/skills/shared/research-quality-rubric.md +165 -0
  252. package/skills/shared/rhetoric-principles.md +54 -0
  253. package/skills/shared/skill-design-patterns.md +272 -0
  254. package/skills/shared/skill-index.md +240 -0
  255. package/skills/shared/system-documentation.md +334 -0
  256. package/skills/shared/tikz-rules.md +402 -0
  257. package/skills/shared/validation-tiers.md +121 -0
  258. package/skills/shared/venue-guides/README.md +46 -0
  259. package/skills/shared/venue-guides/cell_press_style.md +483 -0
  260. package/skills/shared/venue-guides/conferences_formatting.md +564 -0
  261. package/skills/shared/venue-guides/cs_conference_style.md +463 -0
  262. package/skills/shared/venue-guides/examples/cell_summary_example.md +247 -0
  263. package/skills/shared/venue-guides/examples/medical_structured_abstract.md +313 -0
  264. package/skills/shared/venue-guides/examples/nature_abstract_examples.md +213 -0
  265. package/skills/shared/venue-guides/examples/neurips_introduction_example.md +245 -0
  266. package/skills/shared/venue-guides/journals_formatting.md +486 -0
  267. package/skills/shared/venue-guides/medical_journal_styles.md +535 -0
  268. package/skills/shared/venue-guides/ml_conference_style.md +556 -0
  269. package/skills/shared/venue-guides/nature_science_style.md +405 -0
  270. package/skills/shared/venue-guides/reviewer_expectations.md +417 -0
  271. package/skills/shared/venue-guides/venue_writing_styles.md +321 -0
  272. package/skills/split-pdf/SKILL.md +172 -0
  273. package/skills/split-pdf/methodology.md +48 -0
  274. package/skills/sync-notion/SKILL.md +93 -0
  275. package/skills/system-audit/SKILL.md +157 -0
  276. package/skills/system-audit/references/sub-agent-prompts.md +294 -0
  277. package/skills/task-management/SKILL.md +131 -0
  278. package/skills/update-focus/SKILL.md +204 -0
  279. package/skills/update-project-doc/SKILL.md +194 -0
  280. package/skills/validate-bib/SKILL.md +242 -0
  281. package/skills/validate-bib/references/council-mode.md +34 -0
  282. package/skills/validate-bib/references/deep-verify.md +71 -0
  283. package/skills/validate-bib/references/openalex-verification.md +45 -0
  284. package/skills/validate-bib/references/preprint-check.md +31 -0
  285. package/skills/validate-bib/references/report-template.md +62 -0
@@ -0,0 +1,678 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Biblio MCP Server
4
+
5
+ Multi-source scholarly search: OpenAlex (always) + Scopus + Web of Science (when API keys provided).
6
+ Exposes both source-specific openalex_* tools and cross-source scholarly_* tools.
7
+ Imports the shared OpenAlex client from .scripts/openalex/ — single source of truth.
8
+ """
9
+
10
+ import asyncio
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from mcp.server import Server
16
+ from mcp.server.stdio import stdio_server
17
+ from mcp.types import Tool, TextContent
18
+
19
+ # Import existing OpenAlex client and helpers
20
+ SCRIPTS_DIR = str(
21
+ Path(__file__).parent.parent / ".scripts" / "openalex"
22
+ )
23
+ sys.path.insert(0, SCRIPTS_DIR)
24
+
25
+ # Add sources/ to path for multi-source adapters
26
+ SOURCES_DIR = str(Path(__file__).parent)
27
+ if SOURCES_DIR not in sys.path:
28
+ sys.path.insert(0, SOURCES_DIR)
29
+
30
+ from openalex_client import OpenAlexClient # noqa: E402
31
+ from query_helpers import ( # noqa: E402
32
+ find_author_works,
33
+ analyze_research_output,
34
+ get_publication_trends,
35
+ )
36
+
37
+ from formatters import ( # noqa: E402
38
+ format_works_table,
39
+ format_author_profile,
40
+ format_trends,
41
+ format_work_detail,
42
+ )
43
+
44
+ # Multi-source imports
45
+ from sources.openalex_source import OpenAlexSource # noqa: E402
46
+ from sources.multi_source import MultiSource # noqa: E402
47
+ from sources.formatters import ( # noqa: E402
48
+ format_papers_table,
49
+ format_verification_table,
50
+ format_source_status,
51
+ )
52
+
53
+
54
+ def log(msg):
55
+ print(f"[biblio-mcp] {msg}", file=sys.stderr, flush=True)
56
+
57
+
58
+ # Shared client instance (polite pool)
59
+ client = OpenAlexClient(email="user@example.com")
60
+
61
+ # ---------- Multi-source initialization ----------
62
+
63
+ _all_sources = []
64
+ _source_info = []
65
+
66
+ # OpenAlex — always available
67
+ _openalex_source = OpenAlexSource(client)
68
+ _all_sources.append(_openalex_source)
69
+ _source_info.append({"name": "OpenAlex", "key": "openalex", "active": True})
70
+ log("OpenAlex source: active")
71
+
72
+ # Scopus — optional, requires SCOPUS_API_KEY
73
+ _scopus_key = os.environ.get("SCOPUS_API_KEY")
74
+ if _scopus_key:
75
+ from sources.scopus_source import ScopusSource
76
+ _scopus_inst_token = os.environ.get("SCOPUS_INST_TOKEN", "")
77
+ _scopus_source = ScopusSource(_scopus_key, inst_token=_scopus_inst_token)
78
+ _all_sources.append(_scopus_source)
79
+ _source_info.append({"name": "Scopus", "key": "scopus", "active": True})
80
+ log(f"Scopus source: active{' (InstToken)' if _scopus_inst_token else ''}")
81
+ else:
82
+ _source_info.append({"name": "Scopus", "key": "scopus", "active": False})
83
+ log("Scopus source: no API key")
84
+
85
+ # Web of Science — optional, requires WOS_API_KEY
86
+ _wos_key = os.environ.get("WOS_API_KEY")
87
+ _wos_tier = os.environ.get("WOS_API_TIER", "starter").lower()
88
+ if _wos_key:
89
+ from sources.wos_source import WosSource
90
+ _wos_source = WosSource(_wos_key, tier=_wos_tier)
91
+ _all_sources.append(_wos_source)
92
+ _source_info.append({"name": f"Web of Science ({_wos_tier})", "key": "wos", "active": True})
93
+ log(f"WoS source: active (tier={_wos_tier})")
94
+ else:
95
+ _source_info.append({"name": "Web of Science", "key": "wos", "active": False})
96
+ log("WoS source: no API key")
97
+
98
+ # Composite source for cross-source queries
99
+ _multi_source = MultiSource(_all_sources) if len(_all_sources) > 1 else _openalex_source
100
+ log(f"Multi-source: {len(_all_sources)} source(s) active")
101
+
102
+ server = Server("biblio")
103
+ log("Server initialized")
104
+
105
+
106
+ # ---------- Tool definitions ----------
107
+
108
+ TOOLS = [
109
+ Tool(
110
+ name="openalex_search_works",
111
+ description=(
112
+ "Search OpenAlex for scholarly papers by topic. Supports filters for "
113
+ "year range, minimum citations, open access, and sort order. "
114
+ "Returns a markdown table of results."
115
+ ),
116
+ inputSchema={
117
+ "type": "object",
118
+ "properties": {
119
+ "query": {
120
+ "type": "string",
121
+ "description": "Search query (topic, keywords, title fragment)",
122
+ },
123
+ "year": {
124
+ "type": "string",
125
+ "description": "Year filter: e.g. '2023', '>2020', '2020-2024'",
126
+ },
127
+ "min_citations": {
128
+ "type": "integer",
129
+ "description": "Minimum citation count",
130
+ },
131
+ "open_access": {
132
+ "type": "boolean",
133
+ "description": "Only return open access papers",
134
+ },
135
+ "sort": {
136
+ "type": "string",
137
+ "description": "Sort order: 'cited_by_count:desc' (default), 'publication_date:desc', 'relevance_score:desc'",
138
+ },
139
+ "limit": {
140
+ "type": "integer",
141
+ "description": "Max results (default 25, max 50)",
142
+ },
143
+ },
144
+ "required": ["query"],
145
+ },
146
+ ),
147
+ Tool(
148
+ name="openalex_author_works",
149
+ description=(
150
+ "Find publications by a specific author. Searches by name, "
151
+ "resolves to OpenAlex author ID, returns their works."
152
+ ),
153
+ inputSchema={
154
+ "type": "object",
155
+ "properties": {
156
+ "author_name": {
157
+ "type": "string",
158
+ "description": "Author name to search for",
159
+ },
160
+ "limit": {
161
+ "type": "integer",
162
+ "description": "Max results (default 50, max 100)",
163
+ },
164
+ },
165
+ "required": ["author_name"],
166
+ },
167
+ ),
168
+ Tool(
169
+ name="openalex_author_profile",
170
+ description=(
171
+ "Analyze an author's research output: total works, open access %, "
172
+ "publications by year, and top topics."
173
+ ),
174
+ inputSchema={
175
+ "type": "object",
176
+ "properties": {
177
+ "author_name": {
178
+ "type": "string",
179
+ "description": "Author name to analyze",
180
+ },
181
+ "years": {
182
+ "type": "string",
183
+ "description": "Year filter (default: '>2020')",
184
+ },
185
+ },
186
+ "required": ["author_name"],
187
+ },
188
+ ),
189
+ Tool(
190
+ name="openalex_institution_output",
191
+ description=(
192
+ "Analyze an institution's research output: total works, open access %, "
193
+ "publications by year, and top topics."
194
+ ),
195
+ inputSchema={
196
+ "type": "object",
197
+ "properties": {
198
+ "institution_name": {
199
+ "type": "string",
200
+ "description": "Institution name to analyze",
201
+ },
202
+ "years": {
203
+ "type": "string",
204
+ "description": "Year filter (default: '>2020')",
205
+ },
206
+ },
207
+ "required": ["institution_name"],
208
+ },
209
+ ),
210
+ Tool(
211
+ name="openalex_trends",
212
+ description=(
213
+ "Get publication count trends over time for a search term. "
214
+ "Returns yearly publication counts."
215
+ ),
216
+ inputSchema={
217
+ "type": "object",
218
+ "properties": {
219
+ "query": {
220
+ "type": "string",
221
+ "description": "Search term to track trends for",
222
+ },
223
+ },
224
+ "required": ["query"],
225
+ },
226
+ ),
227
+ Tool(
228
+ name="openalex_lookup_doi",
229
+ description=(
230
+ "Look up a work by DOI. Returns full metadata including title, "
231
+ "authors, abstract, citations, and open access status."
232
+ ),
233
+ inputSchema={
234
+ "type": "object",
235
+ "properties": {
236
+ "doi": {
237
+ "type": "string",
238
+ "description": "DOI (with or without https://doi.org/ prefix)",
239
+ },
240
+ },
241
+ "required": ["doi"],
242
+ },
243
+ ),
244
+ Tool(
245
+ name="openalex_citing_works",
246
+ description=(
247
+ "Find papers that cite a given work (forward citation tracking). "
248
+ "Provide a DOI and get back the citing papers."
249
+ ),
250
+ inputSchema={
251
+ "type": "object",
252
+ "properties": {
253
+ "doi": {
254
+ "type": "string",
255
+ "description": "DOI of the work to find citations for",
256
+ },
257
+ "limit": {
258
+ "type": "integer",
259
+ "description": "Max results (default 25, max 50)",
260
+ },
261
+ },
262
+ "required": ["doi"],
263
+ },
264
+ ),
265
+ ]
266
+
267
+
268
+ # ---------- Scholarly tool definitions (cross-source) ----------
269
+
270
+ SCHOLARLY_TOOLS = [
271
+ Tool(
272
+ name="scholarly_search",
273
+ description=(
274
+ "Search for scholarly papers across ALL enabled sources (OpenAlex, Scopus, WoS) "
275
+ "with automatic DOI-based deduplication. Returns merged results with the best "
276
+ "metadata from each source."
277
+ ),
278
+ inputSchema={
279
+ "type": "object",
280
+ "properties": {
281
+ "query": {
282
+ "type": "string",
283
+ "description": "Search query (topic, keywords, title fragment)",
284
+ },
285
+ "year_from": {
286
+ "type": "integer",
287
+ "description": "Start year filter (inclusive)",
288
+ },
289
+ "year_to": {
290
+ "type": "integer",
291
+ "description": "End year filter (inclusive)",
292
+ },
293
+ "sort_by": {
294
+ "type": "string",
295
+ "description": "Sort: 'relevance' (default), 'cited_by_count', 'publication_year'",
296
+ },
297
+ "limit": {
298
+ "type": "integer",
299
+ "description": "Max results (default 25, max 50)",
300
+ },
301
+ },
302
+ "required": ["query"],
303
+ },
304
+ ),
305
+ Tool(
306
+ name="scholarly_verify_dois",
307
+ description=(
308
+ "Batch-verify DOIs across all enabled sources. For each DOI, checks if it exists "
309
+ "in OpenAlex, Scopus, and/or WoS. Returns verification status: VERIFIED (2+ sources), "
310
+ "SINGLE_SOURCE (1 source), or NOT_FOUND. The killer tool for /literature Phase 4."
311
+ ),
312
+ inputSchema={
313
+ "type": "object",
314
+ "properties": {
315
+ "dois": {
316
+ "type": "array",
317
+ "items": {"type": "string"},
318
+ "description": "List of DOIs to verify (up to 50). With or without https://doi.org/ prefix.",
319
+ },
320
+ },
321
+ "required": ["dois"],
322
+ },
323
+ ),
324
+ Tool(
325
+ name="scholarly_similar_works",
326
+ description=(
327
+ "Find papers similar to a given text (title or abstract) across all enabled sources. "
328
+ "Results are deduplicated by DOI."
329
+ ),
330
+ inputSchema={
331
+ "type": "object",
332
+ "properties": {
333
+ "text": {
334
+ "type": "string",
335
+ "description": "Text to find similar papers for (title, abstract, or topic description)",
336
+ },
337
+ "limit": {
338
+ "type": "integer",
339
+ "description": "Max results (default 20, max 50)",
340
+ },
341
+ },
342
+ "required": ["text"],
343
+ },
344
+ ),
345
+ Tool(
346
+ name="scholarly_source_status",
347
+ description=(
348
+ "Show which scholarly data sources are configured and active. "
349
+ "Reports OpenAlex (always), Scopus (if SCOPUS_API_KEY set), "
350
+ "WoS (if WOS_API_KEY set)."
351
+ ),
352
+ inputSchema={
353
+ "type": "object",
354
+ "properties": {},
355
+ },
356
+ ),
357
+ ]
358
+
359
+ # Conditional source-specific tools
360
+ if _scopus_key:
361
+ SCHOLARLY_TOOLS.append(
362
+ Tool(
363
+ name="scholarly_search_scopus",
364
+ description=(
365
+ "Search Scopus directly using Scopus query syntax (TITLE-ABS-KEY). "
366
+ "Useful for ASJC subject codes and Scopus-specific features."
367
+ ),
368
+ inputSchema={
369
+ "type": "object",
370
+ "properties": {
371
+ "query": {
372
+ "type": "string",
373
+ "description": "Search query for Scopus (TITLE-ABS-KEY syntax)",
374
+ },
375
+ "year_from": {"type": "integer", "description": "Start year"},
376
+ "year_to": {"type": "integer", "description": "End year"},
377
+ "limit": {"type": "integer", "description": "Max results (default 25)"},
378
+ },
379
+ "required": ["query"],
380
+ },
381
+ )
382
+ )
383
+
384
+ if _wos_key:
385
+ SCHOLARLY_TOOLS.append(
386
+ Tool(
387
+ name="scholarly_search_wos",
388
+ description=(
389
+ "Search Web of Science directly using WoS query syntax (TS=). "
390
+ "Useful for WoS-specific features and citation tracking."
391
+ ),
392
+ inputSchema={
393
+ "type": "object",
394
+ "properties": {
395
+ "query": {
396
+ "type": "string",
397
+ "description": "Search query for WoS (TS= syntax)",
398
+ },
399
+ "year_from": {"type": "integer", "description": "Start year"},
400
+ "year_to": {"type": "integer", "description": "End year"},
401
+ "limit": {"type": "integer", "description": "Max results (default 25)"},
402
+ },
403
+ "required": ["query"],
404
+ },
405
+ )
406
+ )
407
+
408
+
409
+ @server.list_tools()
410
+ async def list_tools() -> list[Tool]:
411
+ return TOOLS + SCHOLARLY_TOOLS
412
+
413
+
414
+ # ---------- Tool handlers ----------
415
+
416
+
417
+ @server.call_tool()
418
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
419
+ log(f"call_tool: {name} {arguments}")
420
+
421
+ try:
422
+ if name == "openalex_search_works":
423
+ return await _handle_search_works(arguments)
424
+ elif name == "openalex_author_works":
425
+ return await _handle_author_works(arguments)
426
+ elif name == "openalex_author_profile":
427
+ return await _handle_author_profile(arguments)
428
+ elif name == "openalex_institution_output":
429
+ return await _handle_institution_output(arguments)
430
+ elif name == "openalex_trends":
431
+ return await _handle_trends(arguments)
432
+ elif name == "openalex_lookup_doi":
433
+ return await _handle_lookup_doi(arguments)
434
+ elif name == "openalex_citing_works":
435
+ return await _handle_citing_works(arguments)
436
+ # Scholarly (cross-source) tools
437
+ elif name == "scholarly_search":
438
+ return await _handle_scholarly_search(arguments)
439
+ elif name == "scholarly_verify_dois":
440
+ return await _handle_scholarly_verify_dois(arguments)
441
+ elif name == "scholarly_similar_works":
442
+ return await _handle_scholarly_similar_works(arguments)
443
+ elif name == "scholarly_source_status":
444
+ return await _handle_scholarly_source_status(arguments)
445
+ elif name == "scholarly_search_scopus":
446
+ return await _handle_scholarly_search_scopus(arguments)
447
+ elif name == "scholarly_search_wos":
448
+ return await _handle_scholarly_search_wos(arguments)
449
+ else:
450
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
451
+ except Exception as e:
452
+ log(f"Error in {name}: {e}")
453
+ return [TextContent(type="text", text=f"**Error:** {e}")]
454
+
455
+
456
+ async def _handle_search_works(args: dict) -> list[TextContent]:
457
+ query = args["query"]
458
+ limit = min(args.get("limit", 25), 50)
459
+ sort = args.get("sort", "cited_by_count:desc")
460
+
461
+ filter_params: dict[str, str] = {}
462
+ if args.get("year"):
463
+ filter_params["publication_year"] = args["year"]
464
+ if args.get("min_citations"):
465
+ filter_params["cited_by_count"] = f">{args['min_citations']}"
466
+ if args.get("open_access"):
467
+ filter_params["is_oa"] = "true"
468
+
469
+ def _search():
470
+ return client.search_works(
471
+ search=query,
472
+ filter_params=filter_params if filter_params else None,
473
+ per_page=limit,
474
+ sort=sort,
475
+ )
476
+
477
+ response = await asyncio.to_thread(_search)
478
+ works = response.get("results", [])
479
+ total = response.get("meta", {}).get("count", 0)
480
+
481
+ text = format_works_table(works, title=f"Search: {query}")
482
+ text += f"\n\n*{total:,} total results in OpenAlex (showing top {len(works)})*"
483
+ return [TextContent(type="text", text=text)]
484
+
485
+
486
+ async def _handle_author_works(args: dict) -> list[TextContent]:
487
+ author_name = args["author_name"]
488
+ limit = min(args.get("limit", 50), 100)
489
+
490
+ works = await asyncio.to_thread(find_author_works, author_name, client, limit)
491
+ text = format_works_table(works, title=f"Works by {author_name}")
492
+ return [TextContent(type="text", text=text)]
493
+
494
+
495
+ async def _handle_author_profile(args: dict) -> list[TextContent]:
496
+ author_name = args["author_name"]
497
+ years = args.get("years", ">2020")
498
+
499
+ analysis = await asyncio.to_thread(
500
+ analyze_research_output, "author", author_name, client, years
501
+ )
502
+ text = format_author_profile(analysis)
503
+ return [TextContent(type="text", text=text)]
504
+
505
+
506
+ async def _handle_institution_output(args: dict) -> list[TextContent]:
507
+ institution_name = args["institution_name"]
508
+ years = args.get("years", ">2020")
509
+
510
+ analysis = await asyncio.to_thread(
511
+ analyze_research_output, "institution", institution_name, client, years
512
+ )
513
+ text = format_author_profile(analysis)
514
+ return [TextContent(type="text", text=text)]
515
+
516
+
517
+ async def _handle_trends(args: dict) -> list[TextContent]:
518
+ query = args["query"]
519
+
520
+ trends = await asyncio.to_thread(get_publication_trends, query, None, client)
521
+ text = format_trends(trends, search_term=query)
522
+ return [TextContent(type="text", text=text)]
523
+
524
+
525
+ async def _handle_lookup_doi(args: dict) -> list[TextContent]:
526
+ doi = args["doi"]
527
+ if not doi.startswith("https://doi.org/"):
528
+ doi = f"https://doi.org/{doi}"
529
+
530
+ work = await asyncio.to_thread(client.get_entity, "works", doi)
531
+ text = format_work_detail(work)
532
+ return [TextContent(type="text", text=text)]
533
+
534
+
535
+ async def _handle_citing_works(args: dict) -> list[TextContent]:
536
+ doi = args["doi"]
537
+ limit = min(args.get("limit", 25), 50)
538
+
539
+ if not doi.startswith("https://doi.org/"):
540
+ doi = f"https://doi.org/{doi}"
541
+
542
+ work = await asyncio.to_thread(client.get_entity, "works", doi)
543
+ cited_by_url = work.get("cited_by_api_url")
544
+
545
+ if not cited_by_url:
546
+ return [TextContent(type="text", text="No citation data available for this work.")]
547
+
548
+ import requests
549
+
550
+ def _fetch_citing():
551
+ resp = requests.get(
552
+ cited_by_url,
553
+ params={"mailto": client.email, "per-page": limit},
554
+ timeout=30,
555
+ )
556
+ resp.raise_for_status()
557
+ return resp.json()
558
+
559
+ data = await asyncio.to_thread(_fetch_citing)
560
+ citing_works = data.get("results", [])
561
+ total = data.get("meta", {}).get("count", 0)
562
+
563
+ title_text = (work.get("title") or "this work")[:60]
564
+ text = format_works_table(citing_works, title=f"Papers citing: {title_text}")
565
+ text += f"\n\n*{total:,} total citing works (showing {len(citing_works)})*"
566
+ return [TextContent(type="text", text=text)]
567
+
568
+
569
+ # ---------- Scholarly tool handlers ----------
570
+
571
+
572
+ async def _handle_scholarly_search(args: dict) -> list[TextContent]:
573
+ query = args["query"]
574
+ limit = min(args.get("limit", 25), 50)
575
+ year_from = args.get("year_from")
576
+ year_to = args.get("year_to")
577
+ sort_by = args.get("sort_by", "relevance")
578
+
579
+ if isinstance(_multi_source, MultiSource):
580
+ _multi_source.reset_diagnostics()
581
+
582
+ papers = await _multi_source.search_works(
583
+ query, year_from=year_from, year_to=year_to, sort_by=sort_by, limit=limit,
584
+ )
585
+ text = format_papers_table(papers, title=f"Scholarly Search: {query}")
586
+
587
+ if isinstance(_multi_source, MultiSource):
588
+ diag = _multi_source.consume_diagnostics()
589
+ if diag:
590
+ text += f"\n\n*Sources queried: {', '.join(diag['succeeded'])}"
591
+ if diag["failed"]:
592
+ text += f" | Failed: {', '.join(diag['failed'])}"
593
+ text += f" | {len(papers)} results after dedup*"
594
+ else:
595
+ text += f"\n\n*Source: OpenAlex | {len(papers)} results*"
596
+
597
+ return [TextContent(type="text", text=text)]
598
+
599
+
600
+ async def _handle_scholarly_verify_dois(args: dict) -> list[TextContent]:
601
+ dois = args["dois"]
602
+ if len(dois) > 50:
603
+ return [TextContent(type="text", text="**Error:** Maximum 50 DOIs per request.")]
604
+
605
+ results = await _multi_source.batch_verify_dois(dois)
606
+ text = format_verification_table(results)
607
+
608
+ # Add source summary
609
+ active_names = [s["name"] for s in _source_info if s["active"]]
610
+ text += f"\n\n*Checked against: {', '.join(active_names)}*"
611
+
612
+ return [TextContent(type="text", text=text)]
613
+
614
+
615
+ async def _handle_scholarly_similar_works(args: dict) -> list[TextContent]:
616
+ text_query = args["text"]
617
+ limit = min(args.get("limit", 20), 50)
618
+
619
+ papers = await _multi_source.find_similar_works(text_query, limit=limit)
620
+ preview = text_query[:80] + "..." if len(text_query) > 80 else text_query
621
+ text = format_papers_table(papers, title=f"Similar to: {preview}")
622
+ text += f"\n\n*{len(papers)} results*"
623
+
624
+ return [TextContent(type="text", text=text)]
625
+
626
+
627
+ async def _handle_scholarly_source_status(args: dict) -> list[TextContent]:
628
+ text = format_source_status(_source_info)
629
+ active_count = sum(1 for s in _source_info if s["active"])
630
+ text += f"\n\n*{active_count}/{len(_source_info)} sources active*"
631
+ return [TextContent(type="text", text=text)]
632
+
633
+
634
+ async def _handle_scholarly_search_scopus(args: dict) -> list[TextContent]:
635
+ query = args["query"]
636
+ limit = min(args.get("limit", 25), 50)
637
+ year_from = args.get("year_from")
638
+ year_to = args.get("year_to")
639
+
640
+ papers = await _scopus_source.search_works(
641
+ query, year_from=year_from, year_to=year_to, limit=limit,
642
+ )
643
+ text = format_papers_table(papers, title=f"Scopus: {query}")
644
+ text += f"\n\n*{len(papers)} results from Scopus*"
645
+
646
+ return [TextContent(type="text", text=text)]
647
+
648
+
649
+ async def _handle_scholarly_search_wos(args: dict) -> list[TextContent]:
650
+ query = args["query"]
651
+ limit = min(args.get("limit", 25), 50)
652
+ year_from = args.get("year_from")
653
+ year_to = args.get("year_to")
654
+
655
+ papers = await _wos_source.search_works(
656
+ query, year_from=year_from, year_to=year_to, limit=limit,
657
+ )
658
+ text = format_papers_table(papers, title=f"Web of Science: {query}")
659
+ text += f"\n\n*{len(papers)} results from WoS*"
660
+
661
+ return [TextContent(type="text", text=text)]
662
+
663
+
664
+ # ---------- Main ----------
665
+
666
+ async def main():
667
+ log("Starting MCP server...")
668
+ async with stdio_server() as (read_stream, write_stream):
669
+ log("stdio_server ready, running server...")
670
+ await server.run(
671
+ read_stream, write_stream, server.create_initialization_options()
672
+ )
673
+ log("Server stopped")
674
+
675
+
676
+ if __name__ == "__main__":
677
+ log("Main entry point")
678
+ asyncio.run(main())
@@ -0,0 +1,14 @@
1
+ """Multi-source scholarly search adapters."""
2
+
3
+ from sources.models import Paper
4
+ from sources.base import ScholarlySource
5
+ from sources.openalex_source import OpenAlexSource
6
+ from sources.multi_source import MultiSource, deduplicate_papers
7
+
8
+ __all__ = [
9
+ "Paper",
10
+ "ScholarlySource",
11
+ "OpenAlexSource",
12
+ "MultiSource",
13
+ "deduplicate_papers",
14
+ ]