# Directory Structure
```
├── .gitignore
├── .python-version
├── aer_mcp.py
├── app
│ ├── __init__.py
│ ├── aea
│ │ ├── __init__.py
│ │ ├── aej
│ │ │ └── __init__.py
│ │ ├── aer
│ │ │ └── __init__.py
│ │ ├── aeri
│ │ │ └── __init__.py
│ │ ├── app
│ │ │ └── __init__.py
│ │ ├── mac
│ │ │ └── __init__.py
│ │ ├── mapping.py
│ │ ├── mic
│ │ │ └── __init__.py
│ │ └── pol
│ │ └── __init__.py
│ └── utils
│ ├── __init__.py
│ ├── doi.py
│ └── fake_ua.py
├── LICENSE
├── pyproject.toml
├── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.12
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# dev
dev/
# test
test/
tests/
test.py
# VScode
.vscode
# PyCharm
.idea
# OS X
*.DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# AER-MCP
A MCP Server for finding from AEA.
> 🔍 Search papers from AEA with LLM
>
> 🛠️ Still Under Developing
>
> 💡 Inspiration: [arxiv-mcp-server](https://github.com/blazickjp/arxiv-mcp-server)
## 💡 Quickly Start
### Localization
```bash
git clone https://github.com/sepinetam/aer-mcp.git
cd aer-mcp
uv venv .venv
```
Then, config it
```json
{
"mcpServers": {
"aer-mcp": {
"command": "uv",
"args": [
"--directory",
"/path/to/the/repo/",
"run",
"aer_mcp.py"
]
}
}
}
```
## ⛓️ More Academic MCP Servers
- [Stata-MCP](https://github.com/sepinetam/stata-mcp)
- [NBER-MCP](https://github.com/sepinetam/nber-mcp)
- [arxiv-mcp-server](https://github.com/blazickjp/arxiv-mcp-server)
```
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
```python
__version__ = "0.1.0"
```
--------------------------------------------------------------------------------
/app/utils/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "aer-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"bs4>=0.0.2",
"fake-useragent>=2.2.0",
"mcp[cli]>=1.6.0",
"requests>=2.32.3",
"selenium>=4.31.0",
]
```
--------------------------------------------------------------------------------
/app/aea/app/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class APP(AEA):
def __init__(self):
super().__init__()
self.journal = "app"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
```
--------------------------------------------------------------------------------
/app/aea/mac/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class MAC(AEA):
def __init__(self):
super().__init__()
self.journal = "mac"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
```
--------------------------------------------------------------------------------
/app/aea/mic/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class MIC(AEA):
def __init__(self):
super().__init__()
self.journal = "mic"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
```
--------------------------------------------------------------------------------
/app/aea/pol/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class POL(AEA):
def __init__(self):
super().__init__()
self.journal = "pol"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
```
--------------------------------------------------------------------------------
/app/aea/aej/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class AEJ(AEA):
def __init__(self):
super().__init__()
self.journal = "aej"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
```
--------------------------------------------------------------------------------
/app/aea/aeri/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class AERI(AEA):
def __init__(self):
super().__init__()
self.journal = "aeri"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
```
--------------------------------------------------------------------------------
/app/aea/mapping.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : mapping.py
from app.aea import aer, aeri, app, pol, mac, mic
def mapping(class_name: str):
mapping_dict = {
"aer": aer.AER,
"aeri": aeri.AERI,
"app": app.APP,
"pol": pol.POL,
"mac": mac.MAC,
"mic": mic.MIC
}
return mapping_dict.get(class_name)
```
--------------------------------------------------------------------------------
/app/aea/aer/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
from app.aea import AEA
class AER(AEA):
def __init__(self):
super().__init__()
self.journal = "aer"
self.base_url = f"https://www.aeaweb.org/journals/{self.journal}/"
if __name__ == "__main__":
aer = AER()
print(aer.article_path_url("10.1257/aer.115.4.1059"))
```
--------------------------------------------------------------------------------
/app/utils/fake_ua.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : fake_ua.py
import random
from fake_useragent import UserAgent
import time
def get_random_headers():
ua = UserAgent()
user_agent = ua.random
ip = f"{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}"
referers = [
'https://www.google.com/',
'https://www.bing.com/',
'https://www.aeaweb.org/',
'https://scholar.google.com/',
'https://www.statamcp.com/'
]
headers = {
'User-Agent': user_agent,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
# 'Accept-Language': random.choice(languages),
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'X-Forwarded-For': ip,
'X-Real-IP': ip,
'Client-IP': ip,
'Referer': random.choice(referers),
'Cache-Control': 'max-age=0',
'Pragma': 'no-cache'
}
return headers
```
--------------------------------------------------------------------------------
/app/utils/doi.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : doi.py
import requests
from typing import Any
def get_doi_metadata(doi: str) -> Any | None:
"""
Get metadata of academic paper via doi of paper
Args:
doi (str): The doi of paper, e.g. '10.1257/aer.20181249'
Returns:
dict: metadata dictionary
"""
# API URL
base_url = "https://api.crossref.org/works/"
url = base_url + doi
# request header, the official recommend add your email (not necessary)
mail_address = "[email protected]"
headers = {
'User-Agent': f'Python-Script/1.0 (mailto:{mail_address})'
}
try:
# get requests
response = requests.get(url, headers=headers)
# check whether success
if response.status_code == 200:
data = response.json()
return data['message']
else:
print(f"WRONG, STATUS_CODE: {response.status_code}")
return None
except Exception as e:
print(f"WRONG: {str(e)}")
return None
def get_abstract_from_crossref(doi):
url = f"https://api.crossref.org/works/{doi}"
headers = {"Accept": "application/json"}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
if 'abstract' in data['message']:
return data['message']['abstract']
else:
return "Abstract not available in Crossref"
else:
return f"Error: {response.status_code}"
def key_points(doi):
metadata: dict = get_doi_metadata(doi)
_results = {'doi': doi, 'date': metadata['deposited']['date-time'], 'title': metadata['title'],
'authors': metadata['author'], 'resource': metadata['resource'],
'abstract': get_abstract_from_crossref(doi)}
return _results
# 使用示例
if __name__ == "__main__":
sample_doi = "10.1257/aer.20181249"
result = get_doi_metadata(sample_doi)
print(result.keys())
```
--------------------------------------------------------------------------------
/app/aea/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
#
# @Author : Sepine Tam
# @Email : [email protected]
# @File : __init__.py
import requests
from bs4 import BeautifulSoup
from app.utils import fake_ua
class AEA:
def __init__(self):
self.home_url = "https://www.aeaweb.org/"
self.base_url = None
def article_path_url(self, doi):
url = self.home_url + f"articles?id={doi}"
return url
@staticmethod
def _extract_id(response: requests.models.Response) -> list:
if response.status_code != 200:
return []
soup = BeautifulSoup(response.text, 'html.parser')
container = soup.find(class_="journal-article-group")
if not container:
return []
articles = container.find_all('article', class_="journal-article")
article_ids = [article.get('id') for article in articles if article.has_attr('id')]
return article_ids
def search(self,
q: str,
search_in_title: bool = True,
search_in_abstract: bool = True,
search_in_author: bool = True,
jel_class: str = None,
journal: bool = True
) -> list:
if self.base_url:
base_url = self.base_url + "search-results?"
else:
base_url = "https://www.aeaweb.org/journals/aer/search-results?"
params = [f"ArticleSearch[within][articletitle]={int(search_in_title)}",
f"ArticleSearch[within][articleabstract]={int(search_in_abstract)}",
f"ArticleSearch[within][authorlast]={int(search_in_author)}",
f"JelClass[value]={jel_class or 0}",
f"journal={int(journal)}", f"ArticleSearch[q]={q}"]
search_url = base_url + "&".join(params)
fake_headers = fake_ua.get_random_headers()
resp = requests.get(search_url, headers=fake_headers)
return self._extract_id(resp)
if __name__ == "__main__":
sample_doi = "10.1257/aer.115.4.1059"
aea = AEA()
print(aea.search("skill"))
```
--------------------------------------------------------------------------------
/aer_mcp.py:
--------------------------------------------------------------------------------
```python
from app.aea import mapping
from app.utils.doi import key_points
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="AER-MCP")
_journal_dict: dict = {
"American Economic Review": "aer",
"AER: Insight": "aeri",
"AEJ: Applied Economics": "app",
"AEJ: Economic Policy": "pol",
"AEJ: Macroeconomics": "mac",
"AEJ: Microeconomics": "mic",
}
# @mcp.tool()
# def journal_dict() -> dict:
# """
# Get journal list and the short code
#
# Returns:
# The content of journal in type of dict
# """
# return _journal_dict
@mcp.tool()
def aea_search(q: str, journal_list: list = None, max_search: int = None) -> dict:
"""
Search from ARA Journal.
Args:
q (str):
The question for search, the language of search should be English.
journal_list (list):
The journal list which search from,
The default is ['aer']
max_search (int):
The max search results of returns.
Limits the total number of results across all journals and higher speed of run.
Returns:
The search result with dict type.
Notes:
Total optional of journal is listed in ['aer', 'aeri', 'app', 'pol', 'mac', 'mic']
The journal short name stands the follow dictionary:
{
"American Economic Review": "aer",
"AER: Insight": "aeri",
"AEJ: Applied Economics": "app",
"AEJ: Economic Policy": "pol",
"AEJ: Macroeconomics": "mac",
"AEJ: Microeconomics": "mic",
}
Examples:
>>> aea_search("monetary policy")
>>> aea_search("labor market", journal_list=["aer", "mac", "app"])
>>> aea_search("skill and age", journal_list=["aer", "aeri"], max_search=30)
"""
if journal_list is None:
# journal_list = _journal_dict.values()
journal_list = ["aer", "aeri"]
_results: dict = {}
_count = 0
for journal in journal_list:
_results[journal]: list = []
j_con_doi_list: list = mapping.mapping(journal)().search(q=q)
for doi in j_con_doi_list:
if (key := key_points(doi)) is not None:
_results[journal].append(key)
_count += 1
if _count > max_search:
return _results
return _results
if __name__ == "__main__":
mcp.run(transport="stdio")
```