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,28 @@
1
+ """
2
+ Tool registry auto-loader.
3
+
4
+ Importing this package triggers side-effect registration of all tools.
5
+ Conditional tools (ORCID, CORE, Altmetric) register only if their
6
+ API credentials are available — handled inside each module.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ # Ensure the parent directory (package root) is on sys.path
13
+ # so tool modules can `from _app import ...`
14
+ _pkg_root = str(Path(__file__).resolve().parent.parent)
15
+ if _pkg_root not in sys.path:
16
+ sys.path.insert(0, _pkg_root)
17
+
18
+ # Import all tool modules — each registers its tools as a side effect
19
+ from tools import openalex # noqa: F401 (8 tools)
20
+ from tools import scholarly # noqa: F401 (9+ tools)
21
+ from tools import orcid # noqa: F401 (2 tools, conditional)
22
+ from tools import core # noqa: F401 (2 tools, conditional)
23
+ from tools import altmetric # noqa: F401 (2 tools, conditional)
24
+ from tools import openreview # noqa: F401 (3 tools)
25
+ from tools import dblp # noqa: F401 (1 tool)
26
+ from tools import opencitations # noqa: F401 (2 tools)
27
+ from tools import unpaywall # noqa: F401 (1 tool)
28
+ from tools import zenodo # noqa: F401 (2 tools)
@@ -0,0 +1,19 @@
1
+ """Tool registry — maps tool names to handler functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Callable, Awaitable
6
+
7
+ from mcp.types import Tool, TextContent
8
+
9
+ # All registered Tool definitions (populated by tool modules at import time)
10
+ TOOL_DEFINITIONS: list[Tool] = []
11
+
12
+ # name → async handler(args) mapping
13
+ TOOL_REGISTRY: dict[str, Callable[[dict], Awaitable[list[TextContent]]]] = {}
14
+
15
+
16
+ def register(tool: Tool, handler: Callable[[dict], Awaitable[list[TextContent]]]) -> None:
17
+ """Register a tool definition and its handler."""
18
+ TOOL_DEFINITIONS.append(tool)
19
+ TOOL_REGISTRY[tool.name] = handler
@@ -0,0 +1,107 @@
1
+ """Altmetric tools (2 tools, conditional on API key)."""
2
+
3
+ import json
4
+
5
+ from mcp.types import Tool, TextContent
6
+
7
+ from _app import _altmetric_client
8
+ from tools._registry import register
9
+
10
+
11
+ # ---------- Handlers ----------
12
+
13
+
14
+ async def _handle_altmetric_search(args: dict) -> list[TextContent]:
15
+ if not _altmetric_client:
16
+ return [TextContent(type="text", text="**Error:** Altmetric not configured (set ALTMETRIC_API_KEY + ALTMETRIC_API_PASSWORD)")]
17
+
18
+ query = args["query"]
19
+ timeframe = args.get("timeframe", "all")
20
+ limit = min(args.get("limit", 25), 100)
21
+
22
+ outputs = await _altmetric_client.search(query, timeframe=timeframe, limit=limit)
23
+
24
+ if not outputs:
25
+ return [TextContent(type="text", text=f"No Altmetric results for: {query}")]
26
+
27
+ lines = [f"## Altmetric Search: {query}\n"]
28
+ lines.append("| Score | Title | Tweets | News | Policy | Blogs | Wikipedia | Readers |")
29
+ lines.append("|-------|-------|--------|------|--------|-------|-----------|---------|")
30
+
31
+ for o in outputs:
32
+ title = o.title[:50] + ("..." if len(o.title) > 50 else "")
33
+ m = o.mentions
34
+ tweets = m.get("tweet", 0)
35
+ news = m.get("msm", 0)
36
+ policy = m.get("policy", 0)
37
+ blogs = m.get("blog", 0)
38
+ wiki = m.get("wikipedia", 0)
39
+ readers = o.readers.get("mendeley", 0)
40
+ score = f"**{o.altmetric_score:.0f}**" if o.altmetric_score else "—"
41
+ lines.append(f"| {score} | {title} | {tweets} | {news} | {policy} | {blogs} | {wiki} | {readers} |")
42
+
43
+ lines.append(f"\n*{len(outputs)} results from Altmetric Explorer (timeframe: {timeframe})*")
44
+ return [TextContent(type="text", text="\n".join(lines))]
45
+
46
+
47
+ async def _handle_altmetric_attention_summary(args: dict) -> list[TextContent]:
48
+ if not _altmetric_client:
49
+ return [TextContent(type="text", text="**Error:** Altmetric not configured")]
50
+
51
+ query = args["query"]
52
+ timeframe = args.get("timeframe", "all")
53
+
54
+ data = await _altmetric_client.get_attention_summary(query, timeframe=timeframe)
55
+
56
+ if not data:
57
+ return [TextContent(type="text", text=f"No attention data for: {query}")]
58
+
59
+ lines = [f"## Attention Summary: {query}\n"]
60
+ lines.append(f"```json\n{json.dumps(data, indent=2)[:2000]}\n```")
61
+ lines.append(f"\n*Source: Altmetric Explorer (timeframe: {timeframe})*")
62
+ return [TextContent(type="text", text="\n".join(lines))]
63
+
64
+
65
+ # ---------- Registration (conditional) ----------
66
+
67
+ if _altmetric_client:
68
+ register(
69
+ Tool(
70
+ name="altmetric_search",
71
+ description=(
72
+ "Search for research outputs with altmetric attention data. Returns papers "
73
+ "with their altmetric score and mention breakdown (tweets, news, blogs, policy docs, "
74
+ "Wikipedia, Reddit, Bluesky). Use to discover which papers on a topic get the most "
75
+ "real-world attention beyond citations."
76
+ ),
77
+ inputSchema={
78
+ "type": "object",
79
+ "properties": {
80
+ "query": {"type": "string", "description": "Search query (topic, keywords)"},
81
+ "timeframe": {"type": "string", "description": "Time filter: 'all' (default), '1d', '1w', '1m', '3m', '6m', '1y'"},
82
+ "limit": {"type": "integer", "description": "Max results (default 25, max 100)"},
83
+ },
84
+ "required": ["query"],
85
+ },
86
+ ),
87
+ _handle_altmetric_search,
88
+ )
89
+ register(
90
+ Tool(
91
+ name="altmetric_attention_summary",
92
+ description=(
93
+ "Get aggregate attention summary for a research topic. Returns total mentions, "
94
+ "score distribution, and top sources. Use to understand the overall attention "
95
+ "landscape for a field or topic."
96
+ ),
97
+ inputSchema={
98
+ "type": "object",
99
+ "properties": {
100
+ "query": {"type": "string", "description": "Topic to analyze"},
101
+ "timeframe": {"type": "string", "description": "Time filter: 'all' (default), '1d', '1w', '1m', '3m', '6m', '1y'"},
102
+ },
103
+ "required": ["query"],
104
+ },
105
+ ),
106
+ _handle_altmetric_attention_summary,
107
+ )
@@ -0,0 +1,92 @@
1
+ """CORE tools (2 tools, conditional on API key)."""
2
+
3
+ from mcp.types import Tool, TextContent
4
+
5
+ from _app import _core_source, format_papers_table
6
+ from tools._registry import register
7
+
8
+
9
+ # ---------- Handlers ----------
10
+
11
+
12
+ async def _handle_core_search(args: dict) -> list[TextContent]:
13
+ if not _core_source:
14
+ return [TextContent(type="text", text="**Error:** CORE not configured (set CORE_API_KEY)")]
15
+
16
+ query = args["query"]
17
+ limit = min(args.get("limit", 25), 100)
18
+ year_from = args.get("year_from")
19
+ year_to = args.get("year_to")
20
+
21
+ papers = await _core_source.search_works(
22
+ query, year_from=year_from, year_to=year_to, limit=limit
23
+ )
24
+
25
+ if not papers:
26
+ return [TextContent(type="text", text=f"No CORE results for: {query}")]
27
+
28
+ text = format_papers_table(papers, title=f"CORE Search: {query}")
29
+
30
+ with_text = sum(1 for p in papers if p.open_access_url)
31
+ text += f"\n\n*{len(papers)} results from CORE ({with_text} with full text available)*"
32
+ text += "\n*Use `core_get_fulltext` with the CORE ID to retrieve full paper text.*"
33
+ return [TextContent(type="text", text=text)]
34
+
35
+
36
+ async def _handle_core_get_fulltext(args: dict) -> list[TextContent]:
37
+ if not _core_source:
38
+ return [TextContent(type="text", text="**Error:** CORE not configured (set CORE_API_KEY)")]
39
+
40
+ core_id = args["core_id"]
41
+ full_text = await _core_source.get_full_text(core_id)
42
+
43
+ if not full_text:
44
+ return [TextContent(type="text", text=f"No full text available for CORE ID: {core_id}")]
45
+
46
+ if len(full_text) > 50000:
47
+ full_text = full_text[:50000] + f"\n\n... [truncated, {len(full_text)} chars total]"
48
+
49
+ return [TextContent(type="text", text=f"## Full Text (CORE ID: {core_id})\n\n{full_text}")]
50
+
51
+
52
+ # ---------- Registration (conditional) ----------
53
+
54
+ if _core_source:
55
+ register(
56
+ Tool(
57
+ name="core_search_fulltext",
58
+ description=(
59
+ "Search CORE's 431M+ open access records. Unique: returns papers with "
60
+ "full-text content available. Use when you need actual paper text, not just metadata. "
61
+ "Supports year filtering."
62
+ ),
63
+ inputSchema={
64
+ "type": "object",
65
+ "properties": {
66
+ "query": {"type": "string", "description": "Search query (keywords, title fragment)"},
67
+ "year_from": {"type": "integer", "description": "Start year"},
68
+ "year_to": {"type": "integer", "description": "End year"},
69
+ "limit": {"type": "integer", "description": "Max results (default 25, max 100)"},
70
+ },
71
+ "required": ["query"],
72
+ },
73
+ ),
74
+ _handle_core_search,
75
+ )
76
+ register(
77
+ Tool(
78
+ name="core_get_fulltext",
79
+ description=(
80
+ "Get the full text of a paper by CORE ID. Returns the complete paper text. "
81
+ "Use core_search_fulltext first to find the CORE ID (source_id field, format: core:12345)."
82
+ ),
83
+ inputSchema={
84
+ "type": "object",
85
+ "properties": {
86
+ "core_id": {"type": "integer", "description": "CORE work ID (numeric, from source_id field)"},
87
+ },
88
+ "required": ["core_id"],
89
+ },
90
+ ),
91
+ _handle_core_get_fulltext,
92
+ )
@@ -0,0 +1,52 @@
1
+ """DBLP tools (1 tool, always available)."""
2
+
3
+ from mcp.types import Tool, TextContent
4
+
5
+ from _app import _dblp_source, format_papers_table
6
+ from tools._registry import register
7
+
8
+
9
+ # ---------- Handler ----------
10
+
11
+
12
+ async def _handle_dblp_search(args: dict) -> list[TextContent]:
13
+ query = args["query"]
14
+ limit = min(args.get("limit", 25), 1000)
15
+ year_from = args.get("year_from")
16
+ year_to = args.get("year_to")
17
+
18
+ papers = await _dblp_source.search_works(
19
+ query, year_from=year_from, year_to=year_to, limit=limit
20
+ )
21
+
22
+ if not papers:
23
+ return [TextContent(type="text", text=f"No DBLP results for: {query}")]
24
+
25
+ text = format_papers_table(papers, title=f"DBLP: {query}")
26
+ text += f"\n\n*{len(papers)} results from DBLP*"
27
+ return [TextContent(type="text", text=text)]
28
+
29
+
30
+ # ---------- Registration ----------
31
+
32
+ register(
33
+ Tool(
34
+ name="dblp_search",
35
+ description=(
36
+ "Search DBLP for computer science publications. Covers conferences, journals, "
37
+ "books, and theses comprehensively. Free, no auth. Returns title, authors, venue, "
38
+ "year, DOI. Use for CS venue metadata and author publication lists."
39
+ ),
40
+ inputSchema={
41
+ "type": "object",
42
+ "properties": {
43
+ "query": {"type": "string", "description": "Search query (keywords, title, author name)"},
44
+ "year_from": {"type": "integer", "description": "Start year filter"},
45
+ "year_to": {"type": "integer", "description": "End year filter"},
46
+ "limit": {"type": "integer", "description": "Max results (default 25, max 1000)"},
47
+ },
48
+ "required": ["query"],
49
+ },
50
+ ),
51
+ _handle_dblp_search,
52
+ )
@@ -0,0 +1,296 @@
1
+ """OpenAlex + Crossref tools (8 tools, always available)."""
2
+
3
+ import asyncio
4
+
5
+ from mcp.types import Tool, TextContent
6
+
7
+ from _app import (
8
+ client,
9
+ _crossref_source,
10
+ log,
11
+ find_author_works,
12
+ analyze_research_output,
13
+ get_publication_trends,
14
+ format_works_table,
15
+ format_author_profile,
16
+ format_trends,
17
+ format_work_detail,
18
+ )
19
+ from tools._registry import register
20
+
21
+
22
+ # ---------- Handlers ----------
23
+
24
+
25
+ async def _handle_search_works(args: dict) -> list[TextContent]:
26
+ query = args["query"]
27
+ limit = min(args.get("limit", 25), 50)
28
+ sort = args.get("sort", "cited_by_count:desc")
29
+
30
+ filter_params: dict[str, str] = {}
31
+ if args.get("year"):
32
+ filter_params["publication_year"] = args["year"]
33
+ if args.get("min_citations"):
34
+ filter_params["cited_by_count"] = f">{args['min_citations']}"
35
+ if args.get("open_access"):
36
+ filter_params["is_oa"] = "true"
37
+
38
+ def _search():
39
+ return client.search_works(
40
+ search=query,
41
+ filter_params=filter_params if filter_params else None,
42
+ per_page=limit,
43
+ sort=sort,
44
+ )
45
+
46
+ response = await asyncio.to_thread(_search)
47
+ works = response.get("results", [])
48
+ total = response.get("meta", {}).get("count", 0)
49
+
50
+ text = format_works_table(works, title=f"Search: {query}")
51
+ text += f"\n\n*{total:,} total results in OpenAlex (showing top {len(works)})*"
52
+ return [TextContent(type="text", text=text)]
53
+
54
+
55
+ async def _handle_author_works(args: dict) -> list[TextContent]:
56
+ author_name = args["author_name"]
57
+ limit = min(args.get("limit", 50), 100)
58
+
59
+ works = await asyncio.to_thread(find_author_works, author_name, client, limit)
60
+ text = format_works_table(works, title=f"Works by {author_name}")
61
+ return [TextContent(type="text", text=text)]
62
+
63
+
64
+ async def _handle_author_profile(args: dict) -> list[TextContent]:
65
+ author_name = args["author_name"]
66
+ years = args.get("years", ">2020")
67
+
68
+ analysis = await asyncio.to_thread(
69
+ analyze_research_output, "author", author_name, client, years
70
+ )
71
+ text = format_author_profile(analysis)
72
+ return [TextContent(type="text", text=text)]
73
+
74
+
75
+ async def _handle_institution_output(args: dict) -> list[TextContent]:
76
+ institution_name = args["institution_name"]
77
+ years = args.get("years", ">2020")
78
+
79
+ analysis = await asyncio.to_thread(
80
+ analyze_research_output, "institution", institution_name, client, years
81
+ )
82
+ text = format_author_profile(analysis)
83
+ return [TextContent(type="text", text=text)]
84
+
85
+
86
+ async def _handle_trends(args: dict) -> list[TextContent]:
87
+ query = args["query"]
88
+
89
+ trends = await asyncio.to_thread(get_publication_trends, query, None, client)
90
+ text = format_trends(trends, search_term=query)
91
+ return [TextContent(type="text", text=text)]
92
+
93
+
94
+ async def _handle_lookup_doi(args: dict) -> list[TextContent]:
95
+ doi = args["doi"]
96
+ if not doi.startswith("https://doi.org/"):
97
+ doi = f"https://doi.org/{doi}"
98
+
99
+ work = await asyncio.to_thread(client.get_entity, "works", doi)
100
+ text = format_work_detail(work)
101
+ return [TextContent(type="text", text=text)]
102
+
103
+
104
+ async def _handle_citing_works(args: dict) -> list[TextContent]:
105
+ doi = args["doi"]
106
+ limit = min(args.get("limit", 25), 50)
107
+
108
+ if not doi.startswith("https://doi.org/"):
109
+ doi = f"https://doi.org/{doi}"
110
+
111
+ work = await asyncio.to_thread(client.get_entity, "works", doi)
112
+ cited_by_url = work.get("cited_by_api_url")
113
+
114
+ if not cited_by_url:
115
+ return [TextContent(type="text", text="No citation data available for this work.")]
116
+
117
+ import requests
118
+
119
+ def _fetch_citing():
120
+ resp = requests.get(
121
+ cited_by_url,
122
+ params={"mailto": client.email, "per-page": limit},
123
+ timeout=30,
124
+ )
125
+ resp.raise_for_status()
126
+ return resp.json()
127
+
128
+ data = await asyncio.to_thread(_fetch_citing)
129
+ citing_works = data.get("results", [])
130
+ total = data.get("meta", {}).get("count", 0)
131
+
132
+ title_text = (work.get("title") or "this work")[:60]
133
+ text = format_works_table(citing_works, title=f"Papers citing: {title_text}")
134
+ text += f"\n\n*{total:,} total citing works (showing {len(citing_works)})*"
135
+ return [TextContent(type="text", text=text)]
136
+
137
+
138
+ async def _handle_crossref_lookup_doi(args: dict) -> list[TextContent]:
139
+ doi = args["doi"]
140
+ paper = await _crossref_source.verify_doi(doi)
141
+
142
+ if not paper:
143
+ return [TextContent(type="text", text=f"DOI not found in Crossref: {doi}")]
144
+
145
+ lines = [f"## {paper.title}\n"]
146
+ lines.append(f"**Authors:** {', '.join(paper.authors)}")
147
+ lines.append(f"**Year:** {paper.publication_year}")
148
+ lines.append(f"**Citations:** {paper.cited_by_count:,}")
149
+ if paper.source_name:
150
+ lines.append(f"**Journal:** {paper.source_name}")
151
+ if paper.doi:
152
+ lines.append(f"**DOI:** {paper.doi}")
153
+ if paper.abstract:
154
+ lines.append(f"\n**Abstract:** {paper.abstract}")
155
+
156
+ lines.append(f"\n*Source: Crossref (authoritative DOI registry) | Verified: Yes*")
157
+ return [TextContent(type="text", text="\n".join(lines))]
158
+
159
+
160
+ # ---------- Tool definitions + registration ----------
161
+
162
+ _TOOLS = [
163
+ (
164
+ Tool(
165
+ name="openalex_search_works",
166
+ description=(
167
+ "Search OpenAlex for scholarly papers by topic. Supports filters for "
168
+ "year range, minimum citations, open access, and sort order. "
169
+ "Returns a markdown table of results."
170
+ ),
171
+ inputSchema={
172
+ "type": "object",
173
+ "properties": {
174
+ "query": {"type": "string", "description": "Search query (topic, keywords, title fragment)"},
175
+ "year": {"type": "string", "description": "Year filter: e.g. '2023', '>2020', '2020-2024'"},
176
+ "min_citations": {"type": "integer", "description": "Minimum citation count"},
177
+ "open_access": {"type": "boolean", "description": "Only return open access papers"},
178
+ "sort": {"type": "string", "description": "Sort order: 'cited_by_count:desc' (default), 'publication_date:desc', 'relevance_score:desc'"},
179
+ "limit": {"type": "integer", "description": "Max results (default 25, max 50)"},
180
+ },
181
+ "required": ["query"],
182
+ },
183
+ ),
184
+ _handle_search_works,
185
+ ),
186
+ (
187
+ Tool(
188
+ name="openalex_author_works",
189
+ description="Find publications by a specific author. Searches by name, resolves to OpenAlex author ID, returns their works.",
190
+ inputSchema={
191
+ "type": "object",
192
+ "properties": {
193
+ "author_name": {"type": "string", "description": "Author name to search for"},
194
+ "limit": {"type": "integer", "description": "Max results (default 50, max 100)"},
195
+ },
196
+ "required": ["author_name"],
197
+ },
198
+ ),
199
+ _handle_author_works,
200
+ ),
201
+ (
202
+ Tool(
203
+ name="openalex_author_profile",
204
+ description="Analyze an author's research output: total works, open access %, publications by year, and top topics.",
205
+ inputSchema={
206
+ "type": "object",
207
+ "properties": {
208
+ "author_name": {"type": "string", "description": "Author name to analyze"},
209
+ "years": {"type": "string", "description": "Year filter (default: '>2020')"},
210
+ },
211
+ "required": ["author_name"],
212
+ },
213
+ ),
214
+ _handle_author_profile,
215
+ ),
216
+ (
217
+ Tool(
218
+ name="openalex_institution_output",
219
+ description="Analyze an institution's research output: total works, open access %, publications by year, and top topics.",
220
+ inputSchema={
221
+ "type": "object",
222
+ "properties": {
223
+ "institution_name": {"type": "string", "description": "Institution name to analyze"},
224
+ "years": {"type": "string", "description": "Year filter (default: '>2020')"},
225
+ },
226
+ "required": ["institution_name"],
227
+ },
228
+ ),
229
+ _handle_institution_output,
230
+ ),
231
+ (
232
+ Tool(
233
+ name="openalex_trends",
234
+ description="Get publication count trends over time for a search term. Returns yearly publication counts.",
235
+ inputSchema={
236
+ "type": "object",
237
+ "properties": {
238
+ "query": {"type": "string", "description": "Search term to track trends for"},
239
+ },
240
+ "required": ["query"],
241
+ },
242
+ ),
243
+ _handle_trends,
244
+ ),
245
+ (
246
+ Tool(
247
+ name="openalex_lookup_doi",
248
+ description="Look up a work by DOI. Returns full metadata including title, authors, abstract, citations, and open access status.",
249
+ inputSchema={
250
+ "type": "object",
251
+ "properties": {
252
+ "doi": {"type": "string", "description": "DOI (with or without https://doi.org/ prefix)"},
253
+ },
254
+ "required": ["doi"],
255
+ },
256
+ ),
257
+ _handle_lookup_doi,
258
+ ),
259
+ (
260
+ Tool(
261
+ name="openalex_citing_works",
262
+ description="Find papers that cite a given work (forward citation tracking). Provide a DOI and get back the citing papers.",
263
+ inputSchema={
264
+ "type": "object",
265
+ "properties": {
266
+ "doi": {"type": "string", "description": "DOI of the work to find citations for"},
267
+ "limit": {"type": "integer", "description": "Max results (default 25, max 50)"},
268
+ },
269
+ "required": ["doi"],
270
+ },
271
+ ),
272
+ _handle_citing_works,
273
+ ),
274
+ (
275
+ Tool(
276
+ name="crossref_lookup_doi",
277
+ description=(
278
+ "Look up a DOI in Crossref, the authoritative DOI registry. Returns "
279
+ "verified metadata: title, authors, journal, date, abstract, citation count. "
280
+ "Use this to verify a DOI exists and get canonical metadata. More authoritative "
281
+ "than OpenAlex for DOI verification."
282
+ ),
283
+ inputSchema={
284
+ "type": "object",
285
+ "properties": {
286
+ "doi": {"type": "string", "description": "DOI to look up (with or without https://doi.org/ prefix)"},
287
+ },
288
+ "required": ["doi"],
289
+ },
290
+ ),
291
+ _handle_crossref_lookup_doi,
292
+ ),
293
+ ]
294
+
295
+ for tool, handler in _TOOLS:
296
+ register(tool, handler)