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,226 @@
1
+ """
2
+ Biblio MCP Server — shared state
3
+
4
+ Server instance, client initialization, formatters, and helpers.
5
+ Imported by tool modules in tools/.
6
+ """
7
+
8
+ import asyncio
9
+ import os
10
+ import re
11
+ import sys
12
+ import unicodedata
13
+ from pathlib import Path
14
+
15
+ from mcp.server import Server
16
+ from mcp.types import Tool, TextContent
17
+
18
+ from biblio_sources import (
19
+ AltmetricClient,
20
+ CoreSource,
21
+ CrossrefSource,
22
+ DblpSource,
23
+ OpenCitationsClient,
24
+ OpenReviewClient,
25
+ UnpaywallClient,
26
+ ZenodoClient,
27
+ OrcidClient,
28
+ OpenAlexClient,
29
+ OpenAlexSource,
30
+ MultiSource,
31
+ SemanticScholarSource,
32
+ ScopusSource,
33
+ WosSource,
34
+ )
35
+
36
+ # OpenAlex-specific helpers (raw dict API)
37
+ SCRIPTS_DIR = str(Path(__file__).parent.parent.parent / ".scripts" / "openalex")
38
+ sys.path.insert(0, SCRIPTS_DIR)
39
+
40
+ from query_helpers import ( # noqa: E402
41
+ find_author_works,
42
+ analyze_research_output,
43
+ get_publication_trends,
44
+ )
45
+
46
+ from formatters import ( # noqa: E402
47
+ format_works_table,
48
+ format_author_profile,
49
+ format_trends,
50
+ format_work_detail,
51
+ )
52
+
53
+ from scholarly_formatters import ( # noqa: E402
54
+ format_papers_table,
55
+ format_verification_table,
56
+ format_source_status,
57
+ )
58
+
59
+
60
+ def log(msg):
61
+ print(f"[bibliography-mcp] {msg}", file=sys.stderr, flush=True)
62
+
63
+
64
+ # ---------- Client / source initialization ----------
65
+
66
+ # Shared client instance (polite pool)
67
+ client = OpenAlexClient(email="user@example.com")
68
+
69
+ _all_sources = []
70
+ _source_info = []
71
+
72
+ # OpenAlex — always available
73
+ _openalex_source = OpenAlexSource(client)
74
+ _all_sources.append(_openalex_source)
75
+ _source_info.append({"name": "OpenAlex", "key": "openalex", "active": True})
76
+ log("OpenAlex source: active")
77
+
78
+ # Semantic Scholar — always available, optional API key for higher rate limits
79
+ _s2_key = os.environ.get("S2_API_KEY")
80
+ _s2_source = SemanticScholarSource(api_key=_s2_key)
81
+ _all_sources.append(_s2_source)
82
+ _source_info.append({"name": "Semantic Scholar", "key": "s2", "active": True})
83
+ log(f"Semantic Scholar source: active{' (API key)' if _s2_key else ' (no key, 1 req/sec)'}")
84
+
85
+ # Crossref — always available (no API key, polite pool via mailto)
86
+ _crossref_source = CrossrefSource(mailto="user@example.com")
87
+ _all_sources.append(_crossref_source)
88
+ _source_info.append({"name": "Crossref", "key": "crossref", "active": True})
89
+ log("Crossref source: active (authoritative DOI registry)")
90
+
91
+ # Scopus — optional, requires SCOPUS_API_KEY
92
+ _scopus_key = os.environ.get("SCOPUS_API_KEY")
93
+ _scopus_source = None
94
+ if _scopus_key:
95
+ _scopus_inst_token = os.environ.get("SCOPUS_INST_TOKEN", "")
96
+ _scopus_source = ScopusSource(_scopus_key, inst_token=_scopus_inst_token)
97
+ _all_sources.append(_scopus_source)
98
+ _source_info.append({"name": "Scopus", "key": "scopus", "active": True})
99
+ log(f"Scopus source: active{' (InstToken)' if _scopus_inst_token else ''}")
100
+ else:
101
+ _source_info.append({"name": "Scopus", "key": "scopus", "active": False})
102
+ log("Scopus source: no API key")
103
+
104
+ # Web of Science — optional, requires WOS_API_KEY
105
+ _wos_key = os.environ.get("WOS_API_KEY")
106
+ _wos_source = None
107
+ _wos_tier = os.environ.get("WOS_API_TIER", "starter").lower()
108
+ if _wos_key:
109
+ _wos_source = WosSource(_wos_key, tier=_wos_tier)
110
+ _all_sources.append(_wos_source)
111
+ _source_info.append({"name": f"Web of Science ({_wos_tier})", "key": "wos", "active": True})
112
+ log(f"WoS source: active (tier={_wos_tier})")
113
+ else:
114
+ _source_info.append({"name": "Web of Science", "key": "wos", "active": False})
115
+ log("WoS source: no API key")
116
+
117
+ # Composite source for cross-source queries
118
+ _multi_source = MultiSource(_all_sources) if len(_all_sources) > 1 else _openalex_source
119
+ log(f"Multi-source: {len(_all_sources)} source(s) active")
120
+
121
+ # CORE — open access full text, optional API key
122
+ _core_key = os.environ.get("CORE_API_KEY", "")
123
+ _core_source = None
124
+ if _core_key:
125
+ _core_source = CoreSource(api_key=_core_key)
126
+ _all_sources.append(_core_source)
127
+ _source_info.append({"name": "CORE", "key": "core", "active": True})
128
+ log("CORE source: active (431M+ records, full-text access)")
129
+ else:
130
+ _source_info.append({"name": "CORE", "key": "core", "active": False})
131
+ log("CORE source: no API key (set CORE_API_KEY)")
132
+
133
+ # ORCID — researcher profiles, always available if credentials set
134
+ _orcid_client_id = os.environ.get("ORCID_CLIENT_ID", "")
135
+ _orcid_client_secret = os.environ.get("ORCID_CLIENT_SECRET", "")
136
+ _orcid_client = None
137
+ if _orcid_client_id and _orcid_client_secret:
138
+ _orcid_client = OrcidClient(
139
+ client_id=_orcid_client_id,
140
+ client_secret=_orcid_client_secret,
141
+ )
142
+ log("ORCID client: active")
143
+ else:
144
+ log("ORCID client: no credentials (set ORCID_CLIENT_ID + ORCID_CLIENT_SECRET)")
145
+
146
+ # Altmetric Explorer — research attention metrics, optional
147
+ _altmetric_key = os.environ.get("ALTMETRIC_API_KEY", "")
148
+ _altmetric_secret = os.environ.get("ALTMETRIC_API_PASSWORD", "")
149
+ _altmetric_client = None
150
+ if _altmetric_key and _altmetric_secret:
151
+ _altmetric_client = AltmetricClient(api_key=_altmetric_key, api_secret=_altmetric_secret)
152
+ log("Altmetric Explorer client: active")
153
+ else:
154
+ log("Altmetric Explorer client: no credentials (set ALTMETRIC_API_KEY + ALTMETRIC_API_PASSWORD)")
155
+
156
+ # Zenodo — research data repository, always available (no auth)
157
+ _zenodo_client = ZenodoClient()
158
+ log("Zenodo client: active (no auth required)")
159
+
160
+ # Unpaywall — OA PDF resolver, always available (no auth, just email)
161
+ _unpaywall_client = UnpaywallClient(email="user@example.com")
162
+ log("Unpaywall client: active (no auth required)")
163
+
164
+ # OpenCitations — open citation graph, always available (no auth)
165
+ _opencitations_client = OpenCitationsClient()
166
+ log("OpenCitations client: active (no auth required)")
167
+
168
+ # DBLP — CS publications, always available (no auth)
169
+ _dblp_source = DblpSource()
170
+ log("DBLP source: active (no auth required)")
171
+
172
+ # OpenReview — conference submissions and reviews, always available (no auth)
173
+ _openreview_client = OpenReviewClient()
174
+ log("OpenReview client: active (no auth required)")
175
+
176
+ # ---------- Utility: BibTeX key generation ----------
177
+
178
+ _KEY_STOPWORDS = frozenset({
179
+ "a", "an", "the", "and", "or", "of", "in", "on", "for", "to", "with", "by",
180
+ "from", "at", "is", "are", "do", "does", "how", "what", "when", "where",
181
+ })
182
+
183
+
184
+ def generate_bibtex_key(authors: list[str], year: int | None, title: str | None) -> str:
185
+ """Generate a BibTeX key following Google Scholar convention: surnameYearFirstword.
186
+
187
+ Examples:
188
+ ["Albert Einstein"], 1905, "On the Electrodynamics..." -> einstein1905electrodynamics
189
+ ["María García-López"], 2023, "A Study..." -> garcialopez2023study
190
+ """
191
+ # Surname: last token of first author, ASCII-normalized, lowercase
192
+ surname = ""
193
+ if authors:
194
+ name = authors[0].strip()
195
+ # Handle "Last, First" format
196
+ if "," in name:
197
+ surname = name.split(",")[0].strip()
198
+ else:
199
+ surname = name.split()[-1] if name.split() else ""
200
+ surname = unicodedata.normalize("NFKD", surname).encode("ascii", "ignore").decode("ascii")
201
+ surname = re.sub(r"[^a-zA-Z]", "", surname).lower()
202
+ if not surname:
203
+ surname = "unknown"
204
+
205
+ # Year
206
+ year_str = str(year) if year else ""
207
+
208
+ # First significant word of title
209
+ first_word = ""
210
+ if title:
211
+ title_ascii = unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode("ascii")
212
+ words = re.findall(r"[a-zA-Z]+", title_ascii.lower())
213
+ for w in words:
214
+ if w not in _KEY_STOPWORDS and len(w) > 2:
215
+ first_word = w
216
+ break
217
+ if not first_word and words:
218
+ first_word = words[0]
219
+
220
+ return f"{surname}{year_str}{first_word}"
221
+
222
+
223
+ # ---------- Server instance ----------
224
+
225
+ server = Server("bibliography")
226
+ log("Server initialized")
@@ -0,0 +1,158 @@
1
+ """Markdown formatting functions for OpenAlex API results."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def _get_authors_str(work: dict[str, Any], max_authors: int = 3) -> str:
7
+ """Extract author names from a work, truncating if needed."""
8
+ authorships = work.get("authorships", [])
9
+ names = [a["author"]["display_name"] for a in authorships if a.get("author")]
10
+ if len(names) > max_authors:
11
+ return ", ".join(names[:max_authors]) + " et al."
12
+ return ", ".join(names) if names else "Unknown"
13
+
14
+
15
+ def _get_journal(work: dict[str, Any]) -> str:
16
+ """Extract journal/source name from a work."""
17
+ loc = work.get("primary_location") or {}
18
+ source = loc.get("source") or {}
19
+ return source.get("display_name", "")
20
+
21
+
22
+ def _clean_doi(doi: str | None) -> str:
23
+ """Strip https://doi.org/ prefix from DOI."""
24
+ if not doi:
25
+ return ""
26
+ return doi.replace("https://doi.org/", "")
27
+
28
+
29
+ def format_works_table(works: list[dict[str, Any]], title: str = "Results") -> str:
30
+ """Format a list of works as a markdown table."""
31
+ if not works:
32
+ return f"## {title}\n\nNo results found."
33
+
34
+ lines = [
35
+ f"## {title}\n",
36
+ f"**{len(works)} works**\n",
37
+ "| # | Title | Authors | Year | Journal | Cites | DOI |",
38
+ "|---|-------|---------|------|---------|-------|-----|",
39
+ ]
40
+
41
+ for i, w in enumerate(works, 1):
42
+ title_text = (w.get("title") or "Untitled")[:80]
43
+ authors = _get_authors_str(w)
44
+ year = w.get("publication_year", "")
45
+ journal = _get_journal(w)[:30]
46
+ cites = w.get("cited_by_count", 0)
47
+ doi = _clean_doi(w.get("doi"))
48
+ lines.append(f"| {i} | {title_text} | {authors} | {year} | {journal} | {cites} | {doi} |")
49
+
50
+ return "\n".join(lines)
51
+
52
+
53
+ def format_author_profile(analysis: dict[str, Any]) -> str:
54
+ """Format research output analysis as markdown."""
55
+ if "error" in analysis:
56
+ return f"**Error:** {analysis['error']}"
57
+
58
+ lines = [
59
+ f"## {analysis['entity_name']}",
60
+ "",
61
+ f"- **Total works:** {analysis['total_works']}",
62
+ f"- **Open access:** {analysis['open_access_works']} ({analysis['open_access_percentage']}%)",
63
+ "",
64
+ "### Publications by Year",
65
+ "",
66
+ "| Year | Count |",
67
+ "|------|-------|",
68
+ ]
69
+
70
+ for entry in sorted(analysis.get("publications_by_year", []),
71
+ key=lambda x: x.get("key", ""), reverse=True):
72
+ lines.append(f"| {entry.get('key', '')} | {entry.get('count', 0)} |")
73
+
74
+ topics = analysis.get("top_topics", [])
75
+ if topics:
76
+ lines.extend(["", "### Top Topics", "", "| Topic | Count |", "|-------|-------|"])
77
+ for t in topics[:10]:
78
+ name = t.get("key_display_name", t.get("key", ""))
79
+ lines.append(f"| {name} | {t.get('count', 0)} |")
80
+
81
+ return "\n".join(lines)
82
+
83
+
84
+ def format_trends(trends: list[dict[str, Any]], search_term: str = "") -> str:
85
+ """Format publication trends as markdown."""
86
+ if not trends:
87
+ return "No trend data found."
88
+
89
+ header = f"## Publication Trends"
90
+ if search_term:
91
+ header += f": {search_term}"
92
+
93
+ sorted_trends = sorted(trends, key=lambda x: x.get("key", ""), reverse=True)
94
+
95
+ lines = [
96
+ header,
97
+ "",
98
+ "| Year | Publications |",
99
+ "|------|-------------|",
100
+ ]
101
+
102
+ for entry in sorted_trends[:20]:
103
+ lines.append(f"| {entry.get('key', '')} | {entry.get('count', 0)} |")
104
+
105
+ return "\n".join(lines)
106
+
107
+
108
+ def format_work_detail(work: dict[str, Any]) -> str:
109
+ """Format a single work with full metadata."""
110
+ title = work.get("title") or "Untitled"
111
+ authors = _get_authors_str(work, max_authors=10)
112
+ year = work.get("publication_year", "")
113
+ journal = _get_journal(work)
114
+ doi = _clean_doi(work.get("doi"))
115
+ cites = work.get("cited_by_count", 0)
116
+ oa = work.get("open_access", {})
117
+ oa_status = oa.get("oa_status", "unknown")
118
+ oa_url = oa.get("oa_url", "")
119
+
120
+ # Reconstruct abstract from inverted index
121
+ abstract = ""
122
+ inv_index = work.get("abstract_inverted_index")
123
+ if inv_index:
124
+ word_positions: list[tuple[int, str]] = []
125
+ for word, positions in inv_index.items():
126
+ for pos in positions:
127
+ word_positions.append((pos, word))
128
+ word_positions.sort()
129
+ abstract = " ".join(w for _, w in word_positions)
130
+
131
+ lines = [
132
+ f"## {title}",
133
+ "",
134
+ f"**Authors:** {authors}",
135
+ f"**Year:** {year}",
136
+ f"**Journal:** {journal}",
137
+ f"**DOI:** {doi}",
138
+ f"**Citations:** {cites}",
139
+ f"**Open Access:** {oa_status}",
140
+ ]
141
+
142
+ if oa_url:
143
+ lines.append(f"**OA URL:** {oa_url}")
144
+
145
+ work_type = work.get("type", "")
146
+ if work_type:
147
+ lines.append(f"**Type:** {work_type}")
148
+
149
+ if abstract:
150
+ lines.extend(["", "### Abstract", "", abstract])
151
+
152
+ # Concepts/topics
153
+ topics = work.get("topics", [])
154
+ if topics:
155
+ topic_names = [t.get("display_name", "") for t in topics[:5]]
156
+ lines.extend(["", f"**Topics:** {', '.join(topic_names)}"])
157
+
158
+ return "\n".join(lines)
@@ -0,0 +1,35 @@
1
+ # Session Log: 2026-03-13 21:00
2
+
3
+ ## Project: .mcp-server-bibliography
4
+
5
+ > Also logged: Task Management (skills updates) — `../../log/2026-03-13-2100.md`
6
+
7
+ ## What We Did
8
+ - Added Semantic Scholar as 4th source adapter (created in prior session, tested and confirmed working)
9
+ - Rewrote S2 adapter to use `/paper/search/bulk` (recommended by S2) and Recommendations API for `find_similar_works`
10
+ - Added 4 new S2 Graph API methods: `get_paper_citations`, `get_paper_references`, `search_author` + `get_author_papers`, `get_paper_detail`
11
+ - Extended Paper model with `tldr`, `bibtex`, `open_access_url` fields
12
+ - Exposed 4 new MCP tools: `scholarly_citations`, `scholarly_references`, `scholarly_paper_detail`, `scholarly_author_papers`
13
+ - Server now has 17 tools total (7 OpenAlex + 10 scholarly) across 4 sources
14
+ - Fixed S2 field scoping: base fields for bulk endpoints, extended fields (tldr, citationStyles, isOpenAccess, openAccessPdf) only for single paper lookups
15
+ - Fixed citations/references endpoints to use `citingPaper.` / `citedPaper.` field prefixes
16
+ - All 6 test categories passed: bulk search, DOI verify, forward citations, backward references, author search + papers, paper detail with TLDR/BibTeX, recommendations
17
+
18
+ ## Key Decisions
19
+ - S2 Datasets API (bulk download) is NOT suitable for live MCP queries — only Graph + Recommendations are used
20
+ - Extended fields (`tldr`, `citationStyles`) only available on single paper lookup, not bulk/citations/references endpoints — used separate field constants `S2_FIELDS` vs `S2_DETAIL_FIELDS`
21
+ - the user applying for S2 API key for higher rate limits (drafted application answers)
22
+
23
+ ## Problems/Blockers
24
+ - First attempt at extended fields caused 400 errors on bulk search, citations, and references endpoints — resolved by splitting field constants
25
+ - Citations/references endpoints need prefixed nested fields (`citingPaper.title`, `citedPaper.title`) — different from other endpoints
26
+
27
+ ## Next Steps
28
+ - [ ] Add S2_API_KEY to `research/scout/.env` once the user receives it
29
+ - [ ] Consider adding S2 as a source in Scout CLI itself (currently only OpenAlex/Scopus/WoS)
30
+
31
+ ## Files Changed
32
+ - `sources/models.py` — added `tldr`, `bibtex`, `open_access_url` fields to Paper dataclass
33
+ - `sources/semantic_scholar_source.py` — added `S2_DETAIL_FIELDS`, 5 new methods, enriched `_to_paper` with tldr/bibtex/oa_url
34
+ - `server.py` — added 4 new tool definitions and handlers
35
+ - `run.sh` — S2_API_KEY added to env var loading (prior session)
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "biblio-mcp"
3
+ version = "0.2.0"
4
+ description = "Multi-source scholarly search MCP server: OpenAlex + Scopus + Web of Science"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "mcp>=1.0.0",
8
+ "requests",
9
+ "httpx>=0.27",
10
+ "biblio-sources @ git+https://github.com/user/biblio-sources.git@9cff529",
11
+ ]
12
+
13
+ [tool.uv.sources]
14
+ biblio-sources = { path = "../biblio-sources", editable = true }
15
+
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper to launch the bibliography MCP server with API keys from Scout's .env
3
+ set -euo pipefail
4
+
5
+ ENV_FILE="$(dirname "$0")/../../research/scout/.env"
6
+
7
+ if [[ -f "$ENV_FILE" ]]; then
8
+ while IFS='=' read -r key value; do
9
+ # Skip comments and blank lines
10
+ [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
11
+ # Only export the keys the server needs
12
+ case "$key" in
13
+ WOS_API_KEY|WOS_API_TIER|SCOPUS_API_KEY|SCOPUS_INST_TOKEN|S2_API_KEY|ORCID_CLIENT_ID|ORCID_CLIENT_SECRET|CORE_API_KEY|ALTMETRIC_API_KEY|ALTMETRIC_API_PASSWORD)
14
+ export "$key"="$value"
15
+ ;;
16
+ esac
17
+ done < "$ENV_FILE"
18
+ fi
19
+
20
+ exec uv run --directory "$(dirname "$0")" python server.py
@@ -0,0 +1,83 @@
1
+ """Markdown formatters for multi-source scholarly results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from biblio_sources.models import Paper
6
+
7
+
8
+ def format_papers_table(papers: list[Paper], title: str = "Results") -> str:
9
+ """Format a list of Papers as a markdown table."""
10
+ if not papers:
11
+ return f"## {title}\n\nNo results found."
12
+
13
+ lines = [f"## {title}\n"]
14
+ lines.append("| # | Title | Authors | Year | Cited | Source | DOI |")
15
+ lines.append("|---|-------|---------|------|-------|--------|-----|")
16
+
17
+ for i, p in enumerate(papers, 1):
18
+ authors = ", ".join(p.authors[:3])
19
+ if len(p.authors) > 3:
20
+ authors += " et al."
21
+ title_short = p.title[:80] + "..." if len(p.title) > 80 else p.title
22
+ doi_link = f"[link]({p.doi})" if p.doi else "—"
23
+ source = p.source_name or "—"
24
+ if len(source) > 30:
25
+ source = source[:27] + "..."
26
+
27
+ lines.append(
28
+ f"| {i} | {title_short} | {authors} | {p.publication_year} | "
29
+ f"{p.cited_by_count:,} | {source} | {doi_link} |"
30
+ )
31
+
32
+ return "\n".join(lines)
33
+
34
+
35
+ def format_verification_table(results: dict[str, Paper | None]) -> str:
36
+ """Format DOI verification results as a markdown table."""
37
+ lines = ["## DOI Verification Results\n"]
38
+ lines.append("| # | DOI | Title | Year | Cited By | Verified By | Status |")
39
+ lines.append("|---|-----|-------|------|----------|-------------|--------|")
40
+
41
+ verified_count = 0
42
+ single_count = 0
43
+ not_found_count = 0
44
+
45
+ for i, (doi, paper) in enumerate(results.items(), 1):
46
+ if paper is None:
47
+ status = "NOT FOUND"
48
+ not_found_count += 1
49
+ lines.append(f"| {i} | `{doi}` | — | — | — | — | ❌ {status} |")
50
+ else:
51
+ sources = ", ".join(paper.verified_by)
52
+ if len(paper.verified_by) >= 2:
53
+ status = "VERIFIED"
54
+ verified_count += 1
55
+ else:
56
+ status = "SINGLE SOURCE"
57
+ single_count += 1
58
+ title_short = paper.title[:60] + "..." if len(paper.title) > 60 else paper.title
59
+ lines.append(
60
+ f"| {i} | `{doi}` | {title_short} | {paper.publication_year} | "
61
+ f"{paper.cited_by_count:,} | {sources} | "
62
+ f"{'✅' if status == 'VERIFIED' else '⚠️'} {status} |"
63
+ )
64
+
65
+ lines.append("")
66
+ lines.append(f"**Summary:** {verified_count} verified (2+ sources), "
67
+ f"{single_count} single-source, {not_found_count} not found")
68
+
69
+ return "\n".join(lines)
70
+
71
+
72
+ def format_source_status(sources: list[dict]) -> str:
73
+ """Format source status as a markdown table."""
74
+ lines = ["## Scholarly Source Status\n"]
75
+ lines.append("| Source | Status | Key |")
76
+ lines.append("|--------|--------|-----|")
77
+
78
+ for s in sources:
79
+ status = "✅ Active" if s["active"] else "❌ Not configured"
80
+ key = s.get("key", "—")
81
+ lines.append(f"| {s['name']} | {status} | `{key}` |")
82
+
83
+ return "\n".join(lines)