academia-mcp 1.1.4__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
academia_mcp/llm.py ADDED
@@ -0,0 +1,38 @@
1
+ import os
2
+ from typing import List, Dict, Any
3
+
4
+ from pydantic import BaseModel
5
+ from openai import AsyncOpenAI
6
+ from openai.types.chat.chat_completion_message import ChatCompletionMessage
7
+
8
+
9
+ class ChatMessage(BaseModel): # type: ignore
10
+ role: str
11
+ content: str | List[Dict[str, Any]]
12
+
13
+
14
+ ChatMessages = List[ChatMessage]
15
+
16
+
17
+ async def llm_acall(model_name: str, prompt: str) -> str:
18
+ key = os.getenv("OPENROUTER_API_KEY", "")
19
+ assert key, "Please set OPENROUTER_API_KEY in the environment variables"
20
+ base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
21
+
22
+ messages: ChatMessages = [
23
+ ChatMessage(role="user", content=prompt),
24
+ ]
25
+ client = AsyncOpenAI(base_url=base_url, api_key=key)
26
+ response: ChatCompletionMessage = (
27
+ (
28
+ await client.chat.completions.create(
29
+ model=model_name,
30
+ messages=messages,
31
+ temperature=0.0,
32
+ )
33
+ )
34
+ .choices[0]
35
+ .message
36
+ )
37
+ assert response.content, "Response content is None"
38
+ return response.content
academia_mcp/server.py CHANGED
@@ -15,7 +15,12 @@ from .tools.document_qa import document_qa
15
15
  from .tools.md_to_pdf import md_to_pdf
16
16
  from .tools.web_search import web_search, tavily_web_search, exa_web_search, brave_web_search
17
17
  from .tools.visit_webpage import visit_webpage
18
- from .tools.bitflip import propose_improvement_idea, extract_bitflip_info
18
+ from .tools.bitflip import (
19
+ extract_bitflip_info,
20
+ generate_research_proposal,
21
+ score_research_proposals,
22
+ )
23
+
19
24
 
20
25
  load_dotenv()
21
26
 
@@ -53,6 +58,9 @@ def run(
53
58
  server.add_tool(anthology_search)
54
59
  server.add_tool(md_to_pdf)
55
60
  server.add_tool(visit_webpage)
61
+ server.add_tool(extract_bitflip_info)
62
+ server.add_tool(generate_research_proposal)
63
+ server.add_tool(score_research_proposals)
56
64
 
57
65
  if os.getenv("TAVILY_API_KEY"):
58
66
  server.add_tool(tavily_web_search)
@@ -64,8 +72,6 @@ def run(
64
72
  server.add_tool(web_search)
65
73
  if os.getenv("OPENROUTER_API_KEY"):
66
74
  server.add_tool(document_qa)
67
- server.add_tool(propose_improvement_idea)
68
- server.add_tool(extract_bitflip_info)
69
75
 
70
76
  if port is None:
71
77
  port = find_free_port()
@@ -7,7 +7,7 @@ from .document_qa import document_qa
7
7
  from .md_to_pdf import md_to_pdf
8
8
  from .web_search import web_search, tavily_web_search, exa_web_search, brave_web_search
9
9
  from .visit_webpage import visit_webpage
10
- from .bitflip import propose_improvement_idea, extract_bitflip_info
10
+ from .bitflip import extract_bitflip_info, generate_research_proposal, score_research_proposals
11
11
 
12
12
 
13
13
  __all__ = [
@@ -24,6 +24,7 @@ __all__ = [
24
24
  "exa_web_search",
25
25
  "brave_web_search",
26
26
  "visit_webpage",
27
- "propose_improvement_idea",
28
27
  "extract_bitflip_info",
28
+ "generate_research_proposal",
29
+ "score_research_proposals",
29
30
  ]
@@ -4,15 +4,24 @@
4
4
  import json
5
5
  import os
6
6
  import random
7
- from typing import List, Dict, Any
7
+ from typing import List, Optional, Any
8
8
 
9
- from openai import OpenAI
10
9
  from pydantic import BaseModel
11
- from openai.types.chat import ChatCompletionMessage
12
10
  from datasets import load_dataset # type: ignore
13
11
 
14
12
  from academia_mcp.tools.arxiv_download import arxiv_download
15
13
  from academia_mcp.utils import extract_json, encode_prompt
14
+ from academia_mcp.llm import llm_acall
15
+
16
+
17
+ class ProposalDataset:
18
+ dataset: Optional[List[Any]] = None
19
+
20
+ @classmethod
21
+ def get_dataset(cls) -> List[Any]:
22
+ if cls.dataset is None:
23
+ cls.dataset = list(load_dataset("UniverseTBD/hypogen-dr1")["train"])
24
+ return cls.dataset
16
25
 
17
26
 
18
27
  EXTRACT_PROMPT = """
@@ -85,24 +94,98 @@ Try to be as specific as possible.
85
94
  Now, please propose a chain of reasoning that leads to an improvement idea for this Bit:
86
95
  {{bit}}
87
96
 
97
+ {% if additional_context %}Additional context:
98
+ {{additional_context}}{% endif %}
99
+
100
+ Finalize your idea by providing the idea details:
101
+ - Abstract: An abstract that summarizes the proposal in conference format (approximately 250 words).
102
+ - Experiments: A list of experiments that would be conducted to validate the proposal. Ensure these are simple and feasible. Be specific in exactly how you would test the hypothesis, and detail precise algorithmic changes. Include the evaluation metrics you would use.
103
+ - Risks and limitations: A list of potential risks and limitations of the proposal.
104
+
88
105
  Return only the JSON object in this exact format (no extra text):
89
106
  {
90
107
  "chain_of_reasoning": "Chain of reasoning that leads to an improvement idea for this Bit. At least 5 sentences.",
91
108
  "flip": "Innovative approach or solution, in at least two sentences",
92
- "spark": "4-6 word summary"
109
+ "spark": "4-6 word summary",
110
+ "abstract": "An abstract that summarizes the proposal in conference format (approximately 250 words).",
111
+ "experiments": ["...", "..."],
112
+ "risks_and_limitations": "A list of potential risks and limitations of the proposal."
93
113
  }
94
114
  """
95
115
 
96
116
 
97
- class ChatMessage(BaseModel): # type: ignore
98
- role: str
99
- content: str | List[Dict[str, Any]]
117
+ SCORE_PROMPT = """
118
+ You are a highly advanced research assistant.
119
+ You are given a list of research proposals.
120
+ Your task is to score the proposals.
121
+
122
+ Proposals:
123
+ {% for proposal in proposals %}
124
+ - Proposal ID: {{proposal["proposal_id"]}}
125
+ - Spark: {{proposal["spark"]}}
126
+ - Abstract: {{proposal["abstract"]}}
127
+ - Experiments: {{proposal["experiments"]}}
128
+ - Risks and limitations: {{proposal["risks_and_limitations"]}}
129
+ {% endfor %}
130
+
131
+ Here are the criteria:
132
+ - "Strengths": A list of strengths of the proposal.
133
+ - "Weaknesses": A list of weaknesses of the proposal.
134
+ - "Novelty": Is the proposal novel? A rating from 1 to 4 (low, medium, high, very high).
135
+ - "Clarity": Is the proposal clear? A rating from 1 to 4 (low, medium, high, very high).
136
+ - "Significance": Is the proposal significant? A rating from 1 to 4 (low, medium, high, very high).
137
+ - "Feasibility": Is the proposal feasible and easy to implement? A rating from 1 to 4 (low, medium, high, very high).
138
+ - "Soundness": Is the proposal sound? A rating from 1 to 4 (poor, fair, good, excellent).
139
+ - "Overall": A rating from 1 to 10 (very strong reject to award quality).
140
+
141
+ Return only scores for all proposals in this exact format (no extra text):
142
+ [
143
+ {
144
+ "proposal_id": 0,
145
+ "spark": "...",
146
+ "strengths": ["...", "..."],
147
+ "weaknesses": ["...", "..."],
148
+ "novelty": 2,
149
+ "clarity": 2,
150
+ "significance": 2,
151
+ "feasibility": 2,
152
+ "soundness": 2,
153
+ "overall": 5
154
+ },
155
+ ...
156
+ ]
157
+ """
158
+
159
+
160
+ class BitFlipInfo(BaseModel): # type: ignore
161
+ bit: str
162
+ flip: str
163
+ spark: str
164
+
100
165
 
166
+ class Proposal(BaseModel): # type: ignore
167
+ proposal_id: Optional[int] = None
168
+ flip: str
169
+ spark: str
170
+ abstract: str
171
+ experiments: List[str]
172
+ risks_and_limitations: List[str]
101
173
 
102
- ChatMessages = List[ChatMessage]
103
174
 
175
+ class ProposalScores(BaseModel): # type: ignore
176
+ proposal_id: int
177
+ spark: str
178
+ strengths: List[str]
179
+ weaknesses: List[str]
180
+ novelty: int
181
+ clarity: int
182
+ significance: int
183
+ feasibility: int
184
+ soundness: int
185
+ overall: int
104
186
 
105
- def extract_bitflip_info(arxiv_id: str) -> str:
187
+
188
+ async def extract_bitflip_info(arxiv_id: str) -> str:
106
189
  """
107
190
  Extracts the Bit-Flip information from the arXiv paper.
108
191
 
@@ -121,73 +204,79 @@ def extract_bitflip_info(arxiv_id: str) -> str:
121
204
  Args:
122
205
  arxiv_id: The arXiv ID of the paper to extract the Bit-Flip information from.
123
206
  """
124
- base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
125
- key = os.getenv("OPENROUTER_API_KEY", "")
126
- assert key, "Please set OPENROUTER_API_KEY in the environment variables"
127
207
  model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
128
-
129
208
  paper = arxiv_download(arxiv_id)
130
209
  abstract = json.loads(paper)["abstract"]
131
210
  prompt = encode_prompt(EXTRACT_PROMPT, abstract=abstract)
132
- messages: ChatMessages = [
133
- ChatMessage(role="user", content=prompt),
134
- ]
135
- client = OpenAI(base_url=base_url, api_key=key)
136
- response: ChatCompletionMessage = (
137
- client.chat.completions.create(
138
- model=model_name,
139
- messages=messages,
140
- temperature=0.0,
141
- )
142
- .choices[0]
143
- .message
144
- )
145
- assert response.content, "Response content is None"
146
- result = extract_json(response.content)
147
- return json.dumps(result, ensure_ascii=False)
211
+ content = await llm_acall(model_name=model_name, prompt=prompt)
212
+ result = extract_json(content)
213
+ bitflip_info: BitFlipInfo = BitFlipInfo.model_validate(result)
214
+ return str(bitflip_info.model_dump_json())
148
215
 
149
216
 
150
- def propose_improvement_idea(arxiv_id: str) -> str:
217
+ async def generate_research_proposal(bit: str, additional_context: str = "") -> str:
151
218
  """
152
- Proposes an improvement idea for the arXiv paper.
219
+ Proposes an improvement idea for the Bit.
153
220
 
154
- Returns a JSON object in this format:
221
+ Args:
222
+ bit: The Bit to propose an improvement idea for. The bit is a technical limitation or conventional approach of some paper.
223
+ additional_context: Additional context to use when proposing the improvement idea.
224
+
225
+ Returns a JSON string with a research proposal in this format:
155
226
  {
156
- "chain_of_reasoning": "Chain of reasoning that leads to an improvement idea.",
157
- "flip": "Innovative approach or solution",
158
- "spark": "4-6 word summary"
227
+ "proposal_id": ...,
228
+ "flip": "Innovative approach or solution, in at least two sentences",
229
+ "spark": "4-6 word summary",
230
+ "abstract": "An abstract that summarizes the proposal in conference format (approximately 250 words).",
231
+ "experiments": ["...", "..."],
232
+ "risks_and_limitations": "A list of potential risks and limitations of the proposal."
159
233
  }
160
234
  Use `json.loads` to deserialize the result if you want to get specific fields.
161
-
162
- Args:
163
- arxiv_id: The arXiv ID of the paper to propose an improvement idea for.
164
235
  """
165
- base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
166
- key = os.getenv("OPENROUTER_API_KEY", "")
167
- assert key, "Please set OPENROUTER_API_KEY in the environment variables"
168
236
  model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
237
+ examples = ProposalDataset.get_dataset()[:]
238
+ examples = random.choices(examples, k=4)
169
239
 
170
- bitflip_info = json.loads(extract_bitflip_info(arxiv_id))
171
- bit = bitflip_info["bit"]
240
+ prompt = encode_prompt(
241
+ IMPROVEMENT_PROMPT, bit=bit, examples=examples, additional_context=additional_context
242
+ )
243
+ content = await llm_acall(model_name=model_name, prompt=prompt)
244
+ result = extract_json(content)
245
+ proposal: Proposal = Proposal.model_validate(result)
246
+ proposal.proposal_id = random.randint(0, 1000000)
247
+ return str(proposal.model_dump_json())
172
248
 
173
- examples = list(load_dataset("UniverseTBD/hypogen-dr1")["train"])
174
- random.shuffle(examples)
175
- examples = examples[:4]
176
249
 
177
- prompt = encode_prompt(IMPROVEMENT_PROMPT, bit=bit, examples=examples)
178
- messages: ChatMessages = [
179
- ChatMessage(role="user", content=prompt),
250
+ async def score_research_proposals(proposals: List[str]) -> str:
251
+ """
252
+ Scores a list of research proposals.
253
+ Use proposals obtained with the `generate_research_proposal` tool.
254
+
255
+ Returns a JSON string with a list of scores in this format:
256
+ [
257
+ {
258
+ "proposal_id": 0,
259
+ "spark": "...",
260
+ "strengths": ["...", "..."],
261
+ "weaknesses": ["...", "..."],
262
+ "novelty": 2,
263
+ "clarity": 2,
264
+ "significance": 2,
265
+ "feasibility": 2,
266
+ "soundness": 2,
267
+ "overall": 5
268
+ },
269
+ ...
180
270
  ]
181
- client = OpenAI(base_url=base_url, api_key=key)
182
- response: ChatCompletionMessage = (
183
- client.chat.completions.create(
184
- model=model_name,
185
- messages=messages,
186
- temperature=0.0,
187
- )
188
- .choices[0]
189
- .message
190
- )
191
- assert response.content, "Response content is None"
192
- result = extract_json(response.content)
193
- return json.dumps(result, ensure_ascii=False)
271
+ Use `json.loads` to deserialize the result if you want to get specific fields.
272
+
273
+ Args:
274
+ proposals: A list of JSON strings with research proposals.
275
+ """
276
+ model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
277
+ proposals = [Proposal.model_validate_json(proposal) for proposal in proposals]
278
+ prompt = encode_prompt(SCORE_PROMPT, proposals=proposals)
279
+ content = await llm_acall(model_name=model_name, prompt=prompt)
280
+ scores = extract_json(content)
281
+ final_scores = [ProposalScores.model_validate(score) for score in scores]
282
+ return json.dumps([s.model_dump() for s in final_scores], ensure_ascii=False)
@@ -1,18 +1,15 @@
1
1
  import os
2
- from typing import List, Any, Dict, cast
2
+ from typing import List, Any, Dict
3
3
  from dotenv import load_dotenv
4
4
 
5
5
  from pydantic import BaseModel
6
- from openai import OpenAI
7
- from openai.types.chat import ChatCompletionMessageParam, ChatCompletionMessage
8
6
 
7
+ from academia_mcp.llm import llm_acall
9
8
 
10
9
  load_dotenv()
11
10
 
12
- SYSTEM_PROMPT = (
13
- "You are a helpful assistant that answers questions about documents accurately and concisely."
14
- )
15
- PROMPT = """Please answer the following questions based solely on the provided document.
11
+ PROMPT = """You are a helpful assistant that answers questions about documents accurately and concisely.
12
+ Please answer the following questions based solely on the provided document.
16
13
  If there is no answer in the document, output "There is no answer in the provided document".
17
14
  First cite ALL relevant document fragments, then provide a final answer.
18
15
  Answer all given questions one by one.
@@ -40,7 +37,7 @@ class ChatMessage(BaseModel): # type: ignore
40
37
  ChatMessages = List[ChatMessage]
41
38
 
42
39
 
43
- def document_qa(
40
+ async def document_qa(
44
41
  document: str,
45
42
  question: str,
46
43
  ) -> str:
@@ -64,33 +61,7 @@ def document_qa(
64
61
  assert question and question.strip(), "Please provide non-empty 'question'"
65
62
  assert document and document.strip(), "Please provide non-empty 'document'"
66
63
 
67
- base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
68
- key = os.getenv("OPENROUTER_API_KEY", "")
69
- assert key, "Please set OPENROUTER_API_KEY in the environment variables"
70
64
  model_name = os.getenv("DOCUMENT_QA_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
71
-
72
- messages: ChatMessages = [
73
- ChatMessage(role="system", content=SYSTEM_PROMPT),
74
- ChatMessage(
75
- role="user",
76
- content=PROMPT.format(question=question, document=document),
77
- ),
78
- ]
79
-
80
- sdk_messages = [
81
- cast(ChatCompletionMessageParam, m.model_dump(exclude_none=True)) for m in messages
82
- ]
83
- client = OpenAI(base_url=base_url, api_key=key)
84
- response: ChatCompletionMessage = (
85
- client.chat.completions.create(
86
- model=model_name,
87
- messages=sdk_messages,
88
- temperature=0.0,
89
- )
90
- .choices[0]
91
- .message
92
- )
93
-
94
- if response.content is None:
95
- raise Exception("Response content is None")
96
- return response.content.strip()
65
+ prompt = PROMPT.format(question=question, document=document)
66
+ content = await llm_acall(model_name=model_name, prompt=prompt)
67
+ return content.strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: academia-mcp
3
- Version: 1.1.4
3
+ Version: 1.2.0
4
4
  Summary: MCP server that provides different tools to search for scientific publications
5
5
  Author-email: Ilya Gusev <phoenixilya@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/IlyaGusev/academia_mcp
@@ -31,6 +31,7 @@ Requires-Dist: pytest>=8.4.1
31
31
  Requires-Dist: openai>=1.97.1
32
32
  Requires-Dist: jinja2>=3.1.6
33
33
  Requires-Dist: datasets>=4.0.0
34
+ Requires-Dist: pytest-asyncio>=1.1.0
34
35
  Dynamic: license-file
35
36
 
36
37
  # Academia MCP
@@ -1,24 +1,25 @@
1
1
  academia_mcp/__init__.py,sha256=2Ru2I5u4cE7DrkkAsibDUEF1K6sYtqppb9VyFrRoQKI,94
2
2
  academia_mcp/__main__.py,sha256=rcmsOtJd3SA82exjrcGBuxuptcoxF8AXI7jNjiVq2BY,59
3
3
  academia_mcp/files.py,sha256=hI5dj4h0fX8V3DXKI_C8vs1fte2uc9gsBXC6prLV4o4,745
4
+ academia_mcp/llm.py,sha256=o84FQNSbjjVSk9DlvFXWsUDiz5IOaavYU6kOqnPEG7E,1071
4
5
  academia_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- academia_mcp/server.py,sha256=oiwai6-i_DZ8cCaOJuFRBmKyeLPj5vBTQlHxnmfAnYc,2406
6
+ academia_mcp/server.py,sha256=q9bhacVm-8uuIMc_xSeymmVaIB8pQCqfTQx5GU8hhpM,2492
6
7
  academia_mcp/utils.py,sha256=tkHBho-NfzAR8rplFaiRYq4sBmQ9V3JPjDJTDrz58Xs,4041
7
- academia_mcp/tools/__init__.py,sha256=WUhrF0QHq6F3ljsK5Kc7jh1d0beVFeYVhRfKAjXK79M,872
8
+ academia_mcp/tools/__init__.py,sha256=8_8QWPRYmPiUjdiNrQilEEnCRR-UBU7g-56jT52V3VQ,934
8
9
  academia_mcp/tools/anthology_search.py,sha256=_5s8EzdV7NQD_F3bjVH4XlKKHOJlFtWlQVrPbODuc3I,7847
9
10
  academia_mcp/tools/arxiv_download.py,sha256=xanzt77TZBQRngzGbKCRz4Hp-Mwfe_q-46eRW23TpVs,11219
10
11
  academia_mcp/tools/arxiv_search.py,sha256=pzM18qrF3QL03A53w003kE7hQi3s3QKtjgw0m7K88UY,8355
11
- academia_mcp/tools/bitflip.py,sha256=m6qWbPieXUmi-wSiIbBl1wsvEPwEL6m0f0XmM2xIIY0,7353
12
- academia_mcp/tools/document_qa.py,sha256=NrosUBgz__s8jCWpl4x17A4hMIPczFPFlfmR_OspKCQ,3005
12
+ academia_mcp/tools/bitflip.py,sha256=u0hSOPWbnCDu2EbA_RkueX496SvTKz9QhZcXugshSfI,10949
13
+ academia_mcp/tools/document_qa.py,sha256=Z51X_rm9p53SltKQJeN0-1CDrCefiYujap1vZZ88uU8,2132
13
14
  academia_mcp/tools/hf_datasets_search.py,sha256=KiBkqT4rXjEN4oc1AWZOPnqN_Go90TQogY5-DUm3LQo,2854
14
15
  academia_mcp/tools/md_to_pdf.py,sha256=Ovc_-8j7gIZNEM1d0ZDH-8qbtfZLSaNmCm5DQjrtM0k,12810
15
16
  academia_mcp/tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  academia_mcp/tools/s2_citations.py,sha256=dqrBp76RrX1zH2XzcMAoWBbvbtyhxLeF-xnqOKD_JiM,4852
17
18
  academia_mcp/tools/visit_webpage.py,sha256=0zAZYeQxPDu0OjgAAvbMLZh0ttaS5q-_4WhgsEPrbsI,1542
18
19
  academia_mcp/tools/web_search.py,sha256=NAkbXdD9mKxsIXhWN32dRd_EiaB3G6ENy-n-bc7HAaQ,5448
19
- academia_mcp-1.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
20
- academia_mcp-1.1.4.dist-info/METADATA,sha256=g1qVsuRWZCAJ_EIjEdbG89Adq2FJ7Dy1infEIV8i8yQ,1862
21
- academia_mcp-1.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- academia_mcp-1.1.4.dist-info/entry_points.txt,sha256=gxkiKJ74w2FwJpSECpjA3XtCfI5ZfrM6N8cqnwsq4yY,51
23
- academia_mcp-1.1.4.dist-info/top_level.txt,sha256=CzGpRFsRRJRqWEb1e3SUlcfGqRzOxevZGaJWrtGF8W0,13
24
- academia_mcp-1.1.4.dist-info/RECORD,,
20
+ academia_mcp-1.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
+ academia_mcp-1.2.0.dist-info/METADATA,sha256=bVlYEucMW7togYS7J-nwq609Qx-MFCSx8vADvucSvvo,1899
22
+ academia_mcp-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ academia_mcp-1.2.0.dist-info/entry_points.txt,sha256=gxkiKJ74w2FwJpSECpjA3XtCfI5ZfrM6N8cqnwsq4yY,51
24
+ academia_mcp-1.2.0.dist-info/top_level.txt,sha256=CzGpRFsRRJRqWEb1e3SUlcfGqRzOxevZGaJWrtGF8W0,13
25
+ academia_mcp-1.2.0.dist-info/RECORD,,