# Directory Structure

```
├── .gitignore
├── instructions_prompt.md
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── mcp_dblp
│       ├── __init__.py
│       ├── dblp_client.py
│       ├── server.py
│       └── tools.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# UV
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#uv.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# PyPI configuration file
.pypirc

# macOS
.DS_Store

# Project-specific (development artifacts not tracked in git)
CLAUDE.md
test/
paper/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MCP-DBLP

[![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Python Version](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)

A Model Context Protocol (MCP) server that provides access to the DBLP computer science bibliography database for Large Language Models.

<a href="https://glama.ai/mcp/servers/cm42scf3iv">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/cm42scf3iv/badge" alt="MCP-DBLP MCP server" />
</a>

------

## Overview

The MCP-DBLP integrates the DBLP (Digital Bibliography & Library Project) API with LLMs through the Model Context Protocol, enabling AI models to:

- Search and retrieve academic publications from the DBLP database
- Process citations and generate BibTeX entries
- Perform fuzzy matching on publication titles and author names
- Extract and format bibliographic information
- Process embedded references in documents
- Direct BibTeX export that bypasses LLM processing for maximum accuracy

## Features

- Comprehensive search capabilities with boolean queries
- Fuzzy title and author name matching
- BibTeX entry retrieval directly from DBLP
- Publication filtering by year and venue
- Statistical analysis of publication data
- Direct BibTeX export capability that bypasses LLM processing for maximum accuracy

## Available Tools

| Tool Name                 | Description                                        |
| ------------------------- | -------------------------------------------------- |
| `search`                  | Search DBLP for publications using boolean queries |
| `fuzzy_title_search`      | Search publications with fuzzy title matching      |
| `get_author_publications` | Retrieve publications for a specific author        |
| `get_venue_info`          | Get detailed information about a publication venue |
| `calculate_statistics`    | Generate statistics from publication results       |
| `export_bibtex`           | Export BibTeX entries directly from DBLP to files  |


## Feedback

Provide feedback to the author via this [form](https://form.jotform.com/szeider/mcp-dblp-feedback-form). 

## System Requirements

- Python 3.11+
- [uv](https://docs.astral.sh/uv/getting-started/installation/)

------

## Installation

1. Install an MCP-compatible client (e.g., [Claude Desktop app](https://claude.ai/download))

2. Install the MCP-DBLP:

   ```bash
   git clone https://github.com/username/mcp-dblp.git
   cd mcp-dblp
   uv venv
   source .venv/bin/activate 
   uv pip install -e .  
   ```

1. Create the configuration file:

   For macOS/Linux:

```
   ~/Library/Application/Support/Claude/claude_desktop_config.json
```

   For Windows:

```
   %APPDATA%\Claude\claude_desktop_config.json
```

   Add the following content:

```
   {
     "mcpServers": {
       "mcp-dblp": {
         "command": "uv",
         "args": [
           "--directory",
           "/absolute/path/to/mcp-dblp/",
           "run",
           "mcp-dblp",
           "--exportdir",
           "/absolute/path/to/bibtex/export/folder/"
         ]
       }
     }
   }
```
Windows: `C:\\absolute\\path\\to\\mcp-dblp`

------

## Prompt

Included is an [instructions prompt](./instructions_prompt.md) which should be used together with the text containing citations. On Claude Desktop, the instructions prompt is available via the electrical plug icon.


## Tool Details

### search

Search DBLP for publications using a boolean query string.

**Parameters:**

- `query` (string, required): A query string that may include boolean operators 'and' and 'or' (case-insensitive)
- `max_results` (number, optional): Maximum number of publications to return. Default is 10
- `year_from` (number, optional): Lower bound for publication year
- `year_to` (number, optional): Upper bound for publication year
- `venue_filter` (string, optional): Case-insensitive substring filter for publication venues (e.g., 'iclr')
- `include_bibtex` (boolean, optional): Whether to include BibTeX entries in the results. Default is false

### fuzzy_title_search

Search DBLP for publications with fuzzy title matching.

**Parameters:**

- `title` (string, required): Full or partial title of the publication (case-insensitive)
- `similarity_threshold` (number, required): A float between 0 and 1 where 1.0 means an exact match
- `max_results` (number, optional): Maximum number of publications to return. Default is 10
- `year_from` (number, optional): Lower bound for publication year
- `year_to` (number, optional): Upper bound for publication year
- `venue_filter` (string, optional): Case-insensitive substring filter for publication venues
- `include_bibtex` (boolean, optional): Whether to include BibTeX entries in the results. Default is false

### get_author_publications

Retrieve publication details for a specific author with fuzzy matching.

**Parameters:**

- `author_name` (string, required): Full or partial author name (case-insensitive)
- `similarity_threshold` (number, required): A float between 0 and 1 where 1.0 means an exact match
- `max_results` (number, optional): Maximum number of publications to return. Default is 20
- `include_bibtex` (boolean, optional): Whether to include BibTeX entries in the results. Default is false

### get_venue_info

Retrieve detailed information about a publication venue.

**Parameters:**

- `venue_name` (string, required): Venue name or abbreviation (e.g., 'ICLR' or full name)

### calculate_statistics

Calculate statistics from a list of publication results.

**Parameters:**

- `results` (array, required): An array of publication objects, each with at least 'title', 'authors', 'venue', and 'year'

### export_bibtex

Export BibTeX entries directly from DBLP to a local file.

**Parameters:**

- ```
  links
  ```

   

  (string, required): HTML string containing one or more <a href=biburl>key</a> links

  - Example: `"<a href=https://dblp.org/rec/journals/example.bib>Smith2023</a>"`

**Behavior:**

- For each link, the BibTeX entry is fetched directly from DBLP
- Only the citation key is replaced with the key specified in the link text
- All entries are saved to a timestamped .bib file in the folder specified by `--exportdir`
- Returns the full path to the saved file

**Important Note:** The BibTeX entries are fetched directly from DBLP with a 10-second timeout protection and are not processed, modified, or hallucinated by the LLM. This ensures maximum accuracy and trustworthiness of the bibliographic data. Only the citation keys are modified as specified. If a request times out, an error message is included in the output.

------

## Example

### Input text:

> Our exploration focuses on two types of explanation problems, abductive and contrastive, in local and global contexts (Marques-Silva 2023). Abductive explanations (Ignatiev, Narodytska, and Marques-Silva 2019), corresponding to prime-implicant explanations (Shih, Choi, and Darwiche 2018) and sufficient reason explanations (Darwiche and Ji 2022), clarify specific decision-making instances, while contrastive explanations (Miller 2019; Ignatiev et al. 2020), corresponding to necessary reason explanations (Darwiche and Ji 2022), make explicit the reasons behind the non-selection of alternatives. Conversely, global explanations (Ribeiro, Singh, and Guestrin 2016; Ignatiev, Narodytska, and Marques-Silva 2019) aim to unravel models' decision patterns across various inputs.

### Output text:

> Our exploration focuses on two types of explanation problems, abductive and contrastive, in local and global contexts \cite{MarquesSilvaI23}. Abductive explanations \cite{IgnatievNM19}, corresponding to prime-implicant explanations \cite{ShihCD18} and sufficient reason explanations \cite{DarwicheJ22}, clarify specific decision-making instances, while contrastive explanations \cite{Miller19}; \cite{IgnatievNA020}, corresponding to necessary reason explanations \cite{DarwicheJ22}, make explicit the reasons behind the non-selection of alternatives. Conversely, global explanations \cite{Ribeiro0G16}; \cite{IgnatievNM19} aim to unravel models' decision patterns across various inputs.

### Output Bibtex

> All references have been successfully exported to a BibTeX file at: /absolute/path/to/bibtex/20250305_231431.bib

```
@article{MarquesSilvaI23,
 author       = {Jo{\~{a}}o Marques{-}Silva and
                 Alexey Ignatiev},
 title        = {No silver bullet: interpretable {ML} models must be explained},
 journal      = {Frontiers Artif. Intell.},
 volume       = {6},
 year         = {2023},
 url          = {https://doi.org/10.3389/frai.2023.1128212},
 doi          = {10.3389/FRAI.2023.1128212},
 timestamp    = {Tue, 07 May 2024 20:23:47 +0200},
 biburl       = {https://dblp.org/rec/journals/frai/MarquesSilvaI23.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}

@inproceedings{IgnatievNM19,
 author       = {Alexey Ignatiev and
                 Nina Narodytska and
                 Jo{\~{a}}o Marques{-}Silva},
 title        = {Abduction-Based Explanations for Machine Learning Models},
 booktitle    = {The Thirty-Third {AAAI} Conference on Artificial Intelligence, {AAAI}
                 2019, The Thirty-First Innovative Applications of Artificial Intelligence
                 Conference, {IAAI} 2019, The Ninth {AAAI} Symposium on Educational
                 Advances in Artificial Intelligence, {EAAI} 2019, Honolulu, Hawaii,
                 USA, January 27 - February 1, 2019},
 pages        = {1511--1519},
 publisher    = {{AAAI} Press},
 year         = {2019},
 url          = {https://doi.org/10.1609/aaai.v33i01.33011511},
 doi          = {10.1609/AAAI.V33I01.33011511},
 timestamp    = {Mon, 04 Sep 2023 12:29:24 +0200},
 biburl       = {https://dblp.org/rec/conf/aaai/IgnatievNM19.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}

@inproceedings{ShihCD18,
 author       = {Andy Shih and
                 Arthur Choi and
                 Adnan Darwiche},
 editor       = {J{\'{e}}r{\^{o}}me Lang},
 title        = {A Symbolic Approach to Explaining Bayesian Network Classifiers},
 booktitle    = {Proceedings of the Twenty-Seventh International Joint Conference on
                 Artificial Intelligence, {IJCAI} 2018, July 13-19, 2018, Stockholm,
                 Sweden},
 pages        = {5103--5111},
 publisher    = {ijcai.org},
 year         = {2018},
 url          = {https://doi.org/10.24963/ijcai.2018/708},
 doi          = {10.24963/IJCAI.2018/708},
 timestamp    = {Tue, 20 Aug 2019 16:19:08 +0200},
 biburl       = {https://dblp.org/rec/conf/ijcai/ShihCD18.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}

@inproceedings{DarwicheJ22,
 author       = {Adnan Darwiche and
                 Chunxi Ji},
 title        = {On the Computation of Necessary and Sufficient Explanations},
 booktitle    = {Thirty-Sixth {AAAI} Conference on Artificial Intelligence, {AAAI}
                 2022, Thirty-Fourth Conference on Innovative Applications of Artificial
                 Intelligence, {IAAI} 2022, The Twelveth Symposium on Educational Advances
                 in Artificial Intelligence, {EAAI} 2022 Virtual Event, February 22
                 - March 1, 2022},
 pages        = {5582--5591},
 publisher    = {{AAAI} Press},
 year         = {2022},
 url          = {https://doi.org/10.1609/aaai.v36i5.20498},
 doi          = {10.1609/AAAI.V36I5.20498},
 timestamp    = {Mon, 04 Sep 2023 16:50:24 +0200},
 biburl       = {https://dblp.org/rec/conf/aaai/DarwicheJ22.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}

@article{Miller19,
 author       = {Tim Miller},
 title        = {Explanation in artificial intelligence: Insights from the social sciences},
 journal      = {Artif. Intell.},
 volume       = {267},
 pages        = {1--38},
 year         = {2019},
 url          = {https://doi.org/10.1016/j.artint.2018.07.007},
 doi          = {10.1016/J.ARTINT.2018.07.007},
 timestamp    = {Thu, 25 May 2023 12:52:41 +0200},
 biburl       = {https://dblp.org/rec/journals/ai/Miller19.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}

@inproceedings{IgnatievNA020,
 author       = {Alexey Ignatiev and
                 Nina Narodytska and
                 Nicholas Asher and
                 Jo{\~{a}}o Marques{-}Silva},
 editor       = {Matteo Baldoni and
                 Stefania Bandini},
 title        = {From Contrastive to Abductive Explanations and Back Again},
 booktitle    = {AIxIA 2020 - Advances in Artificial Intelligence - XIXth International
                 Conference of the Italian Association for Artificial Intelligence,
                 Virtual Event, November 25-27, 2020, Revised Selected Papers},
 series       = {Lecture Notes in Computer Science},
 volume       = {12414},
 pages        = {335--355},
 publisher    = {Springer},
 year         = {2020},
 url          = {https://doi.org/10.1007/978-3-030-77091-4\_21},
 doi          = {10.1007/978-3-030-77091-4\_21},
 timestamp    = {Tue, 15 Jun 2021 17:23:54 +0200},
 biburl       = {https://dblp.org/rec/conf/aiia/IgnatievNA020.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}

@inproceedings{Ribeiro0G16,
 author       = {Marco T{\'{u}}lio Ribeiro and
                 Sameer Singh and
                 Carlos Guestrin},
 editor       = {Balaji Krishnapuram and
                 Mohak Shah and
                 Alexander J. Smola and
                 Charu C. Aggarwal and
                 Dou Shen and
                 Rajeev Rastogi},
 title        = {"Why Should {I} Trust You?": Explaining the Predictions of Any Classifier},
 booktitle    = {Proceedings of the 22nd {ACM} {SIGKDD} International Conference on
                 Knowledge Discovery and Data Mining, San Francisco, CA, USA, August
                 13-17, 2016},
 pages        = {1135--1144},
 publisher    = {{ACM}},
 year         = {2016},
 url          = {https://doi.org/10.1145/2939672.2939778},
 doi          = {10.1145/2939672.2939778},
 timestamp    = {Fri, 25 Dec 2020 01:14:16 +0100},
 biburl       = {https://dblp.org/rec/conf/kdd/Ribeiro0G16.bib},
 bibsource    = {dblp computer science bibliography, https://dblp.org}
}
```

------

## Disclaimer

This MCP-DBLP is in its prototype stage and should be used with caution. Users are encouraged to experiment, but any use in critical environments is at their own risk.

------

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

------
```

--------------------------------------------------------------------------------
/src/mcp_dblp/__init__.py:
--------------------------------------------------------------------------------

```python
"""
MCP-DBLP - A MCP server providing access to DBLP publication data
"""

from mcp_dblp.server import main

__all__ = ["main"]  # Initialize MCP server package

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[build-system]
requires = ["hatchling>=1.25.0"]
build-backend = "hatchling.build"

[project]
name = "mcp-dblp"
version = "1.1.2"
description = "An MCP server that allows you to search the DBLP computer sceince bibliography."
authors = [
    {name = "Stefan Szeider", email = "stefan@szeider.net", url = "https://www.ac.tuwien.ac.at/people/szeider/"}
]
requires-python = ">=3.11"
dependencies = [
    "mcp>=1.20.0",
    "requests>=2.32.5"
]

[project.scripts]
mcp-dblp = "mcp_dblp.server:main"

[tool.black]
line-length = 88

[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = [
    "E",     # pycodestyle errors
    "W",     # pycodestyle warnings
    "F",     # pyflakes
    "I",     # isort
    "N",     # pep8-naming
    "UP",    # pyupgrade
    "B",     # flake8-bugbear
    "T20",   # flake8-print (detect print statements)
    "SIM",   # flake8-simplify
]
ignore = [
    "E501",  # Line too long (let formatter handle it)
]

[tool.ruff.lint.per-file-ignores]
"test/**/*.py" = ["T20"]  # Allow print statements in tests

[dependency-groups]
dev = [
    "pytest>=8.4.2",
    "pytest-asyncio>=1.2.0",
    "ruff>=0.14.2",
]



```

--------------------------------------------------------------------------------
/src/mcp_dblp/tools.py:
--------------------------------------------------------------------------------

```python
import json
import subprocess
import time


def run_mcp_call(tool, arguments):
    process = subprocess.Popen(
        ["python", "src/mcp_dblp/server.py"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        bufsize=1,
    )

    time.sleep(0.5)

    message = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "call_tool",
        "params": {"name": tool, "arguments": arguments},
    }

    json_content = json.dumps(message)
    content_length = len(json_content)
    request = f"Content-Length: {content_length}\r\n\r\n{json_content}"

    try:
        process.stdin.write(request)
        process.stdin.flush()

        headers = {}
        while True:
            line = process.stdout.readline().strip()
            if not line:
                break
            parts = line.split(":", 1)
            if len(parts) == 2:
                headers[parts[0].strip().lower()] = parts[1].strip()

        content_length = int(headers.get("content-length", 0))
        process.stdout.read(content_length) if content_length > 0 else ""

    except Exception:
        pass
    finally:
        process.terminate()


if __name__ == "__main__":
    run_mcp_call("echo", {})

```

--------------------------------------------------------------------------------
/instructions_prompt.md:
--------------------------------------------------------------------------------

```markdown
# DBLP Citation Processor Instructions

You are given a text with embedded references in some format, for instance (author, year), with or without a publication list at the end.

## Your Task

1. Retrieve for each citation the matching DBLP entry
2. Extract the COMPLETE and UNMODIFIED BibTeX entry for each citation directly from DBLP
3. Output the text with each citation replaced by a \cite{..} command
4. Output the BibTeX file containing all citations
5. Save the bibtex file using export_bibtex tool

## Important Requirements

- Use ONLY the DBLP search tool to find entries - never create citations yourself!
- **USE BATCH/PARALLEL CALLS**: When you have multiple citations to search, make parallel tool calls in a SINGLE request rather than sequential calls. This is much more efficient.
  - Example: Search for 5 different papers in one request with 5 parallel search calls
  - You can mix different tool types: searches + author lookups + venue info in one batch
- BibTeX entries MUST be copied EXACTLY and COMPLETELY as they appear in DBLP (including all fields, formatting, and whitespace)
- The ONLY modification allowed is changing the citation key:
  - For example, change "DBLP:conf/sat/Szeider09" to just "Szeider09"
  - Ensure all keys remain unique
- If uncertain about the correct entry for a citation, ask the user for guidance
- Do not abbreviate, summarize, or reformat any part of the BibTeX entries

## Search Strategy

### Best Practices for Finding Papers

**For most reliable results, use author name + year in your query:**
- ✅ Good: `search("Vaswani 2017")` or `search("author:Vaswani year:2017")`
- ✅ Good: `search("Attention is All You Need Vaswani")`
- ⚠️ Less reliable: `search("Attention is All You Need")` (may return derivative papers first)

**Why this matters:** DBLP's search ranking doesn't always prioritize the original paper when searching by title alone. Adding author name or year dramatically improves result quality.

### Progressive Search Strategy

When searching for citations, use this progression:

1. **Start with author + year**: `search("Smith 2023")` or `search("author:Smith year:2023")`
2. **Add title keywords**: `search("Smith transformer 2023")`
3. **Try fuzzy_title_search with author hint**: If you know the exact title, use `fuzzy_title_search("Attention is All You Need", similarity_threshold=0.7)` but note that adding author/year to the title helps significantly
4. **Use get_author_publications**: For specific authors, `get_author_publications("Yoshua Bengio", similarity_threshold=0.8)` retrieves their papers directly
5. **Try different name formats**: Try full name, last name only, or name variations

### Tool Selection Guide

- **search()**: Best for author+year, keywords, or general queries. Supports boolean operators (AND, OR)
- **fuzzy_title_search()**: Use when you have the exact title. Works best when DBLP ranking is good, but has been improved to try multiple year ranges automatically
- **get_author_publications()**: Best for retrieving all papers by a specific author with fuzzy name matching

### When to Give Up

Only mark a citation as [CITATION NOT FOUND] after attempting at least 3 different search queries with varying levels of specificity. For important citations that seem to be missing, consider asking the user for more detailed information about the reference.

If you cannot find a citation on DBLP, indicate this by adding [CITATION NOT FOUND] in the text. Also note if you found a citation but you are not certain it is the right one by adding [CHECK] in the text.

## Final Output

When presenting your solution, provide:

1. The processed text with proper \cite{} commands
2. The complete BibTeX file with entries preserving DBLP's exact format
3. Save the bibtex file using export_bibtex

## Available Tools

This system provides the following tools to help with citation processing:

1. **search**: Search DBLP for publications using boolean queries
   - Parameters: query (required), max_results, year_from, year_to, venue_filter, include_bibtex
2. **fuzzy_title_search**: Search publications with fuzzy title matching
   - Parameters: title (required), similarity_threshold (required), max_results, year_from, year_to, venue_filter, include_bibtex
3. **get_author_publications**: Retrieve publications for a specific author
   - Parameters: author_name (required), similarity_threshold (required), max_results, include_bibtex
4. **get_venue_info**: Get detailed information about a publication venue
   - Parameters: venue_name (required)
5. **calculate_statistics**: Generate statistics from publication results
   - Parameters: results (required)
6. **export_bibtex**: Export BibTeX entries from a collection of HTML links into a file.
   - Parameters: links (required) - HTML string containing one or more <a href=biburl>key</a> links
   - Example: "<a href=https://dblp.org/rec/journals/example.bib>Smith23</a>"
   - You can provide the bibtex key, the rest remains exactly as retrieved from DBLP
   - The tool fetches BibTeX entries, replaces citation keys, and saves to a timestamped .bib file
   - Returns the path to the saved file

## Efficiency: Use Parallel Tool Calls

**IMPORTANT**: The MCP protocol supports batching multiple tool calls in a single request. When processing multiple citations:

✅ **DO THIS** (Efficient - Single Request):
```
Make parallel calls in one request:
- search(query="author:Smith year:2023")
- search(query="author:Jones year:2022")
- get_author_publications(author_name="McKay", similarity_threshold=0.8)
All execute simultaneously and return together
```

❌ **DON'T DO THIS** (Inefficient - Multiple Requests):
```
Make sequential calls:
1. search(query="author:Smith year:2023"), wait for response
2. search(query="author:Jones year:2022"), wait for response
3. get_author_publications(...), wait for response
```

**Benefits of batching:**
- 3x-10x faster for multiple citations
- Single round trip instead of multiple
- Works with any combination of the 6 DBLP tools
- Example: Process 10 citations in one batch instead of 10 sequential calls


```

--------------------------------------------------------------------------------
/src/mcp_dblp/dblp_client.py:
--------------------------------------------------------------------------------

```python
import contextlib
import difflib
import logging
import re
from collections import Counter
from typing import Any

import requests

logger = logging.getLogger("dblp_client")

# Default timeout for all HTTP requests
REQUEST_TIMEOUT = 10  # seconds

# Headers for DBLP API requests
# DBLP recommends using an identifying User-Agent to avoid rate-limiting
# See: https://dblp.org/faq/1474706.html
HEADERS = {
    "User-Agent": "mcp-dblp/1.1.1 (https://github.com/szeider/mcp-dblp)",
    "Accept": "application/json",
}


def _fetch_publications(single_query: str, max_results: int) -> list[dict[str, Any]]:
    """Helper function to fetch publications for a single query string."""
    results = []
    try:
        url = "https://dblp.org/search/publ/api"
        params = {"q": single_query, "format": "json", "h": max_results}
        response = requests.get(url, params=params, headers=HEADERS, timeout=REQUEST_TIMEOUT)
        response.raise_for_status()
        data = response.json()
        hits = data.get("result", {}).get("hits", {})
        total = int(hits.get("@total", "0"))
        logger.info(f"Found {total} results for query: {single_query}")
        if total > 0:
            publications = hits.get("hit", [])
            if not isinstance(publications, list):
                publications = [publications]
            for pub in publications:
                info = pub.get("info", {})
                authors = []
                authors_data = info.get("authors", {}).get("author", [])
                if not isinstance(authors_data, list):
                    authors_data = [authors_data]
                for author in authors_data:
                    if isinstance(author, dict):
                        authors.append(author.get("text", ""))
                    else:
                        authors.append(str(author))

                # Extract the proper DBLP URL or ID for BibTeX retrieval
                dblp_url = info.get("url", "")
                dblp_key = ""

                if dblp_url:
                    # Extract the key from the URL (e.g., https://dblp.org/rec/journals/jmlr/ChowdheryNDBMGMBCDDRSSTWPLLNSZDYJGKPSN23)
                    dblp_key = dblp_url.replace("https://dblp.org/rec/", "")
                elif "key" in pub:
                    dblp_key = pub.get("key", "").replace("dblp:", "")
                else:
                    dblp_key = pub.get("@id", "").replace("dblp:", "")

                result = {
                    "title": info.get("title", ""),
                    "authors": authors,
                    "venue": info.get("venue", ""),
                    "year": int(info.get("year", 0)) if info.get("year") else None,
                    "type": info.get("type", ""),
                    "doi": info.get("doi", ""),
                    "ee": info.get("ee", ""),
                    "url": info.get("url", ""),
                    "dblp_key": dblp_key,  # Use more specific name for the DBLP key
                }
                results.append(result)
    except requests.exceptions.Timeout:
        logger.error(f"Timeout error searching DBLP after {REQUEST_TIMEOUT} seconds")
        # Provide timeout error information
        timeout_msg = f"ERROR: Query '{single_query}' timed out after {REQUEST_TIMEOUT} seconds"
        results.append(
            {
                "title": timeout_msg,
                "authors": [],
                "venue": "Error",
                "year": None,
                "error": f"Timeout after {REQUEST_TIMEOUT} seconds",
            }
        )
    except Exception as e:
        logger.error(f"Error searching DBLP: {e}")
        # Return error result instead of mock data
        error_msg = f"ERROR: DBLP API error for query '{single_query}': {str(e)}"
        results.append(
            {
                "title": error_msg,
                "authors": [],
                "venue": "Error",
                "year": None,
                "error": str(e),
            }
        )
    return results


def search(
    query: str,
    max_results: int = 10,
    year_from: int | None = None,
    year_to: int | None = None,
    venue_filter: str | None = None,
    include_bibtex: bool = False,
) -> list[dict[str, Any]]:
    """
    Search DBLP using their public API.

    Parameters:
        query (str): The search query string.
        max_results (int, optional): Maximum number of results to return. Default is 10.
        year_from (int, optional): Lower bound for publication year.
        year_to (int, optional): Upper bound for publication year.
        venue_filter (str, optional): Case-insensitive substring filter
            for publication venues.
        include_bibtex (bool, optional): Whether to include BibTeX entries
            in the results. Default is False.

    Returns:
        List[Dict[str, Any]]: A list of publication dictionaries.
    """
    query_lower = query.lower()
    if "(" in query or ")" in query:
        logger.warning(
            "Parentheses are not supported in boolean queries. "
            "They will be treated as literal characters."
        )
    results = []
    if " or " in query_lower:
        subqueries = [q.strip() for q in query_lower.split(" or ") if q.strip()]
        seen = set()
        for q in subqueries:
            for pub in _fetch_publications(q, max_results):
                identifier = (pub.get("title"), pub.get("year"))
                if identifier not in seen:
                    results.append(pub)
                    seen.add(identifier)
    else:
        results = _fetch_publications(query, max_results)

    filtered_results = []
    for result in results:
        if year_from or year_to:
            year = result.get("year")
            if year:
                try:
                    year = int(year)
                    if (year_from and year < year_from) or (year_to and year > year_to):
                        continue
                except (ValueError, TypeError):
                    pass
        if venue_filter:
            venue = result.get("venue", "")
            if venue_filter.lower() not in venue.lower():
                continue
        filtered_results.append(result)

    if not filtered_results:
        logger.info("No results found. Consider revising your query syntax.")

    filtered_results = filtered_results[:max_results]

    # Fetch BibTeX entries if requested
    if include_bibtex:
        for result in filtered_results:
            if "dblp_key" in result and result["dblp_key"]:
                result["bibtex"] = fetch_bibtex_entry(result["dblp_key"])

    return filtered_results


def get_author_publications(
    author_name: str,
    similarity_threshold: float,
    max_results: int = 20,
    include_bibtex: bool = False,
) -> dict[str, Any]:
    """
    Get publication information for a specific author with fuzzy matching.

    Parameters:
        author_name (str): Author name to search for.
        similarity_threshold (float): Threshold for fuzzy matching (0-1).
        max_results (int, optional): Maximum number of results to return. Default is 20.
        include_bibtex (bool, optional): Whether to include BibTeX entries. Default is False.

    Returns:
        Dict[str, Any]: Dictionary with author publication information.
    """
    logger.info(
        f"Getting publications for author: {author_name} with similarity threshold {similarity_threshold}"
    )
    author_query = f"author:{author_name}"
    publications = search(author_query, max_results=max_results * 2)

    filtered_publications = []
    for pub in publications:
        best_ratio = 0.0
        for candidate in pub.get("authors", []):
            ratio = difflib.SequenceMatcher(None, author_name.lower(), candidate.lower()).ratio()
            if ratio > best_ratio:
                best_ratio = ratio
        if best_ratio >= similarity_threshold:
            filtered_publications.append(pub)

    filtered_publications = filtered_publications[:max_results]

    # Fetch BibTeX entries if requested
    if include_bibtex:
        for pub in filtered_publications:
            if "dblp_key" in pub and pub["dblp_key"]:
                pub["bibtex"] = fetch_bibtex_entry(pub["dblp_key"])

    venues = Counter([p.get("venue", "") for p in filtered_publications])
    years = Counter([p.get("year", "") for p in filtered_publications])
    types = Counter([p.get("type", "") for p in filtered_publications])

    return {
        "name": author_name,
        "publication_count": len(filtered_publications),
        "publications": filtered_publications,
        "stats": {
            "venues": venues.most_common(5),
            "years": years.most_common(5),
            "types": dict(types),
        },
    }


def get_title_publications(
    title_query: str, similarity_threshold: float, max_results: int = 20
) -> list[dict[str, Any]]:
    """
    Retrieve publications whose titles fuzzy-match the given title_query.

    Parameters:
        title_query (str): The title string to search for.
        similarity_threshold (float): A compulsory threshold (0 <= threshold <= 1), where 1.0 means an exact match.
        max_results (int): Maximum number of matching publications to return (default is 20).

    Returns:
        List[Dict[str, Any]]: A list of publication dictionaries that have a title similarity ratio
                              greater than or equal to the threshold.

    Announcement:
        We are pleased to announce a new fuzzy title matching tool in MCP-DBLP.
        With get_title_publications(), users can now search for publications by title using
        a similarity threshold (with 1.0 indicating an exact match). This enhancement ensures that
        minor variations or misspellings in publication titles do not prevent relevant results from being returned.
    """
    candidates = search(title_query, max_results=max_results * 2)
    filtered = []

    for pub in candidates:
        pub_title = pub.get("title", "")
        ratio = difflib.SequenceMatcher(None, title_query.lower(), pub_title.lower()).ratio()
        if ratio >= similarity_threshold:
            pub["title_similarity"] = ratio
            filtered.append(pub)

    filtered = sorted(filtered, key=lambda x: x["title_similarity"], reverse=True)

    return filtered[:max_results]


def fuzzy_title_search(
    title: str,
    similarity_threshold: float,
    max_results: int = 10,
    year_from: int | None = None,
    year_to: int | None = None,
    venue_filter: str | None = None,
    include_bibtex: bool = False,
) -> list[dict[str, Any]]:
    """
    Search DBLP for publications with fuzzy title matching.

    Uses multiple search strategies to improve recall:
    1. Search with "title:" prefix
    2. Search without prefix (broader matching)
    3. Calculate similarity scores and rank by best match

    Note: DBLP's search ranking may not prioritize the exact paper you're looking for.
    For best results, include author name or year in the title parameter
    (e.g., "Attention is All You Need Vaswani" or use the regular search() function).

    Parameters:
        title (str): Full or partial title of the publication (case-insensitive).
        similarity_threshold (float): A float between 0 and 1 where 1.0 means an exact match.
        max_results (int, optional): Maximum number of publications to return. Default is 10.
        year_from (int, optional): Lower bound for publication year.
        year_to (int, optional): Upper bound for publication year.
        venue_filter (str, optional): Case-insensitive substring filter for publication venues.
        include_bibtex (bool, optional): Whether to include BibTeX entries. Default is False.

    Returns:
        List[Dict[str, Any]]: A list of publication objects sorted by title similarity score.
    """
    logger.info(f"Searching for title: '{title}' with similarity threshold {similarity_threshold}")

    candidates = []
    seen_titles = set()

    # Strategy 1: Search with title prefix
    title_query = f"title:{title}"
    results = search(
        title_query,
        max_results=max_results * 3,
        year_from=year_from,
        year_to=year_to,
        venue_filter=venue_filter,
    )
    for pub in results:
        t = pub.get("title", "")
        if t not in seen_titles:
            candidates.append(pub)
            seen_titles.add(t)

    # Strategy 2: Search without prefix
    results = search(
        title,
        max_results=max_results * 2,
        year_from=year_from,
        year_to=year_to,
        venue_filter=venue_filter,
    )
    for pub in results:
        t = pub.get("title", "")
        if t not in seen_titles:
            candidates.append(pub)
            seen_titles.add(t)

    # Calculate similarity scores
    filtered = []
    for pub in candidates:
        pub_title = pub.get("title", "")
        ratio = difflib.SequenceMatcher(None, title.lower(), pub_title.lower()).ratio()
        if ratio >= similarity_threshold:
            pub["similarity"] = ratio
            filtered.append(pub)

    # Sort by similarity score (highest first)
    filtered = sorted(filtered, key=lambda x: x.get("similarity", 0), reverse=True)

    filtered = filtered[:max_results]

    # Fetch BibTeX entries if requested
    if include_bibtex:
        for pub in filtered:
            if "dblp_key" in pub and pub["dblp_key"]:
                bibtex = fetch_bibtex_entry(pub["dblp_key"])
                if bibtex:
                    pub["bibtex"] = bibtex

    return filtered


def fetch_and_process_bibtex(url, new_key):
    """
    Fetch BibTeX from URL and replace the key with new_key.

    Parameters:
        url (str): URL to the BibTeX file
        new_key (str): New citation key to replace the original one

    Returns:
        str: BibTeX content with replaced citation key, or error message
    """
    try:
        response = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
        response.raise_for_status()
        bibtex = response.text

        # Replace the key in format @TYPE{KEY, ... -> @TYPE{new_key, ...
        bibtex = re.sub(r"@(\w+){([^,]+),", r"@\1{" + new_key + ",", bibtex, count=1)
        return bibtex
    except requests.exceptions.Timeout:
        logger.error(f"Timeout fetching {url} after {REQUEST_TIMEOUT} seconds")
        return f"% Error: Timeout fetching {url} after {REQUEST_TIMEOUT} seconds"
    except Exception as e:
        logger.error(f"Error fetching {url}: {str(e)}", exc_info=True)
        return f"% Error fetching {url}: {str(e)}"


def fetch_bibtex_entry(dblp_key: str) -> str:
    """
    Fetch BibTeX entry from DBLP by key.

    Parameters:
        dblp_key (str): DBLP publication key.

    Returns:
        str: BibTeX entry, or empty string if not found.
    """
    try:
        # Make sure we have a valid key
        if not dblp_key or dblp_key.isspace():
            logger.warning("Empty or invalid DBLP key provided")
            return ""

        # Try multiple URL formats to increase chances of success
        urls_to_try = []

        # Format 1: Direct key
        urls_to_try.append(f"https://dblp.org/rec/{dblp_key}.bib")

        # Format 2: If the key has slashes, it might be a full path
        if "/" in dblp_key:
            urls_to_try.append(f"https://dblp.org/rec/{dblp_key}.bib")

        # Format 3: If the key has a colon, it might be a DBLP-style key
        if ":" in dblp_key:
            clean_key = dblp_key.replace(":", "/")
            urls_to_try.append(f"https://dblp.org/rec/{clean_key}.bib")

        # Try each URL until one works
        for url in urls_to_try:
            logger.info(f"Fetching BibTeX from: {url}")
            response = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
            logger.info(f"Response status: {response.status_code}")

            if response.status_code == 200:
                bibtex = response.text
                if not bibtex or bibtex.isspace():
                    logger.warning(f"Received empty BibTeX content for URL: {url}")
                    continue

                logger.info(f"BibTeX content (first 100 chars): {bibtex[:100]}")

                # Extract the citation type and key (e.g., @article{DBLP:journals/jmlr/ChowdheryNDBMGMBCDDRSSTWPLLNSZDYJGKPSN23,)
                citation_key_match = re.match(r"@(\w+){([^,]+),", bibtex)
                if citation_key_match:
                    citation_type = citation_key_match.group(1)
                    old_key = citation_key_match.group(2)
                    logger.info(f"Found citation type: {citation_type}, key: {old_key}")

                    # Create a new key based on the first author's last name and year
                    # Try to extract author and year from the DBLP key or from the BibTeX content
                    author_year_match = re.search(r"([A-Z][a-z]+).*?(\d{2,4})", dblp_key)

                    if author_year_match:
                        author = author_year_match.group(1)
                        year = author_year_match.group(2)
                        if len(year) == 2:  # Convert 2-digit year to 4-digit
                            year = "20" + year if int(year) < 50 else "19" + year
                        new_key = f"{author}{year}"
                        logger.info(f"Generated new key: {new_key}")
                    else:
                        # If we can't extract from key, create a simpler key from the DBLP key
                        parts = dblp_key.split("/")
                        new_key = parts[-1] if parts else dblp_key
                        logger.info(f"Using fallback key: {new_key}")

                    # Replace the old key with the new key
                    bibtex = bibtex.replace(f"{{{old_key},", f"{{{new_key},", 1)
                    logger.info("Replaced old key with new key")

                    return bibtex
                else:
                    logger.warning(
                        f"Could not parse citation key pattern from BibTeX: {bibtex[:100]}..."
                    )
                    return bibtex  # Return the original if we couldn't parse it

        # If we've tried all URLs and none worked
        logger.warning(
            f"Failed to fetch BibTeX for key: {dblp_key} after trying multiple URL formats"
        )
        return ""

    except requests.exceptions.Timeout:
        logger.error(f"Timeout fetching BibTeX for {dblp_key} after {REQUEST_TIMEOUT} seconds")
        return f"% Error: Timeout fetching BibTeX for {dblp_key} after {REQUEST_TIMEOUT} seconds"
    except Exception as e:
        logger.error(f"Error fetching BibTeX for {dblp_key}: {str(e)}", exc_info=True)
        return (
            f"% Error: An unexpected error occurred while fetching BibTeX for {dblp_key}: {str(e)}"
        )


def get_venue_info(venue_name: str) -> dict[str, Any]:
    """
    Get information about a publication venue using DBLP venue search API.
    Returns venue name, acronym, type, and DBLP URL.
    """
    logger.info(f"Getting information for venue: {venue_name}")
    try:
        url = "https://dblp.org/search/venue/api"
        params = {"q": venue_name, "format": "json", "h": 1}
        response = requests.get(url, params=params, headers=HEADERS, timeout=REQUEST_TIMEOUT)
        response.raise_for_status()
        data = response.json()

        hits = data.get("result", {}).get("hits", {})
        total = int(hits.get("@total", "0"))

        if total > 0:
            hit = hits.get("hit", [])
            if isinstance(hit, list):
                hit = hit[0]

            info = hit.get("info", {})
            return {
                "venue": info.get("venue", ""),
                "acronym": info.get("acronym", ""),
                "type": info.get("type", ""),
                "url": info.get("url", ""),
            }
        else:
            logger.warning(f"No venue found for: {venue_name}")
            return {
                "venue": "",
                "acronym": "",
                "type": "",
                "url": "",
            }
    except Exception as e:
        logger.error(f"Error fetching venue info for {venue_name}: {str(e)}")
        return {
            "venue": "",
            "acronym": "",
            "type": "",
            "url": "",
        }


def calculate_statistics(results: list[dict[str, Any]]) -> dict[str, Any]:
    """
    Calculate statistics from publication results.
    (Documentation omitted for brevity)
    """
    logger.info(f"Calculating statistics for {len(results)} results")
    authors = Counter()
    venues = Counter()
    years = []

    for result in results:
        for author in result.get("authors", []):
            authors[author] += 1

        venue = result.get("venue", "")
        # Handle venue as list or string
        if isinstance(venue, list):
            venue = ", ".join(venue) if venue else ""
        if venue:
            venues[venue] += 1
        else:
            venues["(empty)"] += 1

        year = result.get("year")
        if year:
            with contextlib.suppress(ValueError, TypeError):
                years.append(int(year))

    stats = {
        "total_publications": len(results),
        "time_range": {"min": min(years) if years else None, "max": max(years) if years else None},
        "top_authors": sorted(authors.items(), key=lambda x: x[1], reverse=True),
        "top_venues": sorted(venues.items(), key=lambda x: x[1], reverse=True),
    }

    return stats

```

--------------------------------------------------------------------------------
/src/mcp_dblp/server.py:
--------------------------------------------------------------------------------

```python
"""
MCP-DBLP Server Module

IMPORTANT: This file must define a 'main()' function that is imported by __init__.py!
Removing or renaming this function will break package imports and cause an error:
  ImportError: cannot import name 'main' from 'mcp_dblp.server'
"""

import argparse
import asyncio
import datetime
import logging
import os
import re
import sys
from pathlib import Path

import mcp.server.stdio
import mcp.types as types

# Import MCP SDK
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions

# Import DBLP client functions
from mcp_dblp.dblp_client import (
    calculate_statistics,
    fetch_and_process_bibtex,
    fuzzy_title_search,
    get_author_publications,
    get_venue_info,
    search,
)

# Set up logging
log_dir = os.path.expanduser("~/.mcp-dblp")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "mcp_dblp_server.log")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.FileHandler(log_file), logging.StreamHandler(sys.stderr)],
)
logger = logging.getLogger("mcp_dblp")


try:
    from importlib.metadata import version

    version_str = version("mcp-dblp")
    logger.info(f"Loaded version: {version_str}")
except Exception:
    version_str = "x.x.x"  # Anonymous fallback version
    logger.warning(f"Using default version: {version_str}")


def parse_html_links(html_string):
    """Parse HTML links of the form <a href=biburl>key</a> and extract URLs and keys."""
    pattern = r"<a\s+href=([^>]+)>([^<]+)</a>"
    matches = re.findall(pattern, html_string)
    result = []
    for url, key in matches:
        url = url.strip("\"'")
        key = key.strip()
        result.append((url, key))
    return result


def export_bibtex_entries(entries, export_dir):
    """Export BibTeX entries to a file with timestamp filename."""
    os.makedirs(export_dir, exist_ok=True)

    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"{timestamp}.bib"
    filepath = os.path.join(export_dir, filename)

    with open(filepath, "w", encoding="utf-8") as f:
        for entry in entries:
            f.write(entry + "\n\n")

    return filepath


async def serve(export_dir=None) -> None:
    """Main server function to handle MCP requests"""
    if export_dir is None:
        export_dir = os.path.expanduser("~/.mcp-dblp/exports")

    server = Server("mcp-dblp")

    # Provide a list of available prompts including our instructions prompt.
    @server.list_prompts()
    async def handle_list_prompts() -> list[types.Prompt]:
        return [
            types.Prompt(
                name="dblp-instructions",
                description="Instructions for using DBLP tools efficiently with batch/parallel calls for citation processing",
                arguments=[],
            )
        ]

    # Get prompt endpoint that loads our instructions from a file.
    @server.get_prompt()
    async def handle_get_prompt(name: str, arguments: dict | None = None) -> types.GetPromptResult:
        try:
            # Assume instructions_prompt.md is located at the project root
            instructions_path = Path(__file__).resolve().parents[2] / "instructions_prompt.md"
            with open(instructions_path, encoding="utf-8") as f:
                instructions_prompt = f.read()
        except Exception as e:
            instructions_prompt = f"Error loading instructions prompt: {e}"
        return types.GetPromptResult(
            description="Instructions for using DBLP tools efficiently with batch/parallel calls for citation processing",
            messages=[
                types.PromptMessage(
                    role="user", content=types.TextContent(type="text", text=instructions_prompt)
                )
            ],
        )

    # Expose instructions as a resource so it appears in ListMcpResourcesTool
    @server.list_resources()
    async def handle_list_resources() -> list[types.Resource]:
        return [
            types.Resource(
                uri="dblp://instructions",
                name="DBLP Citation Processing Instructions",
                description="Complete instructions for using DBLP tools efficiently with batch/parallel calls",
                mimeType="text/markdown",
            )
        ]

    @server.read_resource()
    async def handle_read_resource(uri: str) -> str:
        uri_str = str(uri) if not isinstance(uri, str) else uri
        if uri_str == "dblp://instructions":
            try:
                instructions_path = Path(__file__).resolve().parents[2] / "instructions_prompt.md"
                with open(instructions_path, encoding="utf-8") as f:
                    return f.read()
            except Exception as e:
                return f"Error loading instructions: {e}"
        else:
            raise ValueError(f"Unknown resource URI: {uri_str}")

    @server.list_tools()
    async def list_tools() -> list[types.Tool]:
        """List all available DBLP tools with detailed descriptions."""
        return [
            types.Tool(
                name="search",
                description=(
                    "Search DBLP for publications using a boolean query string.\n"
                    "Arguments:\n"
                    "  - query (string, required): A query string that may include boolean operators 'and' and 'or' (case-insensitive).\n"
                    "    For example, 'Swin and Transformer'. Parentheses are not supported.\n"
                    "  - max_results (number, optional): Maximum number of publications to return. Default is 10.\n"
                    "  - year_from (number, optional): Lower bound for publication year.\n"
                    "  - year_to (number, optional): Upper bound for publication year.\n"
                    "  - venue_filter (string, optional): Case-insensitive substring filter for publication venues (e.g., 'iclr').\n"
                    "  - include_bibtex (boolean, optional): Whether to include BibTeX entries in the results. Default is false.\n"
                    "Returns a list of publication objects including title, authors, venue, year, type, doi, ee, and url."
                ),
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"},
                        "max_results": {"type": "number"},
                        "year_from": {"type": "number"},
                        "year_to": {"type": "number"},
                        "venue_filter": {"type": "string"},
                        "include_bibtex": {"type": "boolean"},
                    },
                    "required": ["query"],
                },
            ),
            types.Tool(
                name="fuzzy_title_search",
                description=(
                    "Search DBLP for publications with fuzzy title matching.\n"
                    "Arguments:\n"
                    "  - title (string, required): Full or partial title of the publication (case-insensitive).\n"
                    "  - similarity_threshold (number, required): A float between 0 and 1 where 1.0 means an exact match.\n"
                    "  - max_results (number, optional): Maximum number of publications to return. Default is 10.\n"
                    "  - year_from (number, optional): Lower bound for publication year.\n"
                    "  - year_to (number, optional): Upper bound for publication year.\n"
                    "  - venue_filter (string, optional): Case-insensitive substring filter for publication venues.\n"
                    "  - include_bibtex (boolean, optional): Whether to include BibTeX entries in the results. Default is false.\n"
                    "Returns a list of publication objects sorted by title similarity score."
                ),
                inputSchema={
                    "type": "object",
                    "properties": {
                        "title": {"type": "string"},
                        "similarity_threshold": {"type": "number"},
                        "max_results": {"type": "number"},
                        "year_from": {"type": "number"},
                        "year_to": {"type": "number"},
                        "venue_filter": {"type": "string"},
                        "include_bibtex": {"type": "boolean"},
                    },
                    "required": ["title", "similarity_threshold"],
                },
            ),
            types.Tool(
                name="get_author_publications",
                description=(
                    "Retrieve publication details for a specific author with fuzzy matching.\n"
                    "Arguments:\n"
                    "  - author_name (string, required): Full or partial author name (case-insensitive).\n"
                    "  - similarity_threshold (number, required): A float between 0 and 1 where 1.0 means an exact match.\n"
                    "  - max_results (number, optional): Maximum number of publications to return. Default is 20.\n"
                    "  - include_bibtex (boolean, optional): Whether to include BibTeX entries in the results. Default is false.\n"
                    "Returns a dictionary with keys: name, publication_count, publications, and stats (which includes top venues, years, and types)."
                ),
                inputSchema={
                    "type": "object",
                    "properties": {
                        "author_name": {"type": "string"},
                        "similarity_threshold": {"type": "number"},
                        "max_results": {"type": "number"},
                        "include_bibtex": {"type": "boolean"},
                    },
                    "required": ["author_name", "similarity_threshold"],
                },
            ),
            types.Tool(
                name="get_venue_info",
                description=(
                    "Retrieve information about a publication venue from DBLP.\n"
                    "Arguments:\n"
                    "  - venue_name (string, required): Venue name or abbreviation (e.g., 'ICLR', 'NeurIPS', or full name).\n"
                    "Returns a dictionary with fields:\n"
                    "  - venue: Full venue title\n"
                    "  - acronym: Venue acronym/abbreviation (if available)\n"
                    "  - type: Venue type (e.g., 'Conference or Workshop', 'Journal', 'Repository')\n"
                    "  - url: Canonical DBLP URL for the venue\n"
                    "Note: Publisher, ISSN, and other metadata are not available through this endpoint."
                ),
                inputSchema={
                    "type": "object",
                    "properties": {"venue_name": {"type": "string"}},
                    "required": ["venue_name"],
                },
            ),
            types.Tool(
                name="calculate_statistics",
                description=(
                    "Calculate statistics from a list of publication results.\n"
                    "Arguments:\n"
                    "  - results (array, required): An array of publication objects, each with at least 'title', 'authors', 'venue', and 'year'.\n"
                    "Returns a dictionary with:\n"
                    "  - total_publications: Total count.\n"
                    "  - time_range: Dictionary with 'min' and 'max' publication years.\n"
                    "  - top_authors: List of tuples (author, count) sorted by count.\n"
                    "  - top_venues: List of tuples (venue, count) sorted by count (empty venue is treated as '(empty)')."
                ),
                inputSchema={
                    "type": "object",
                    "properties": {"results": {"type": "array"}},
                    "required": ["results"],
                },
            ),
            types.Tool(
                name="export_bibtex",
                description=(
                    "Export BibTeX entries from a collection of HTML hyperlinks.\n"
                    "Arguments:\n"
                    "  - links (string, required): HTML string containing one or more <a href=biburl>key</a> links.\n"
                    "    The href attribute should contain a URL to a BibTeX file, and the link text is used as the citation key.\n"
                    "    Example input with three links:\n"
                    '    "<a href=https://dblp.org/rec/journals/example1.bib>Smith2023</a>\n'
                    "     <a href=https://dblp.org/rec/conf/example2.bib>Jones2022</a>\n"
                    '     <a href=https://dblp.org/rec/journals/example3.bib>Brown2021</a>"\n'
                    "Process:\n"
                    "  - For each link, the tool fetches the BibTeX content from the URL\n"
                    "  - The citation key in each BibTeX entry is replaced with the key from the link text\n"
                    "  - All entries are combined and saved to a .bib file with a timestamp filename\n"
                    "Returns:\n"
                    "  - A message with the full path to the saved .bib file"
                ),
                inputSchema={
                    "type": "object",
                    "properties": {"links": {"type": "string"}},
                    "required": ["links"],
                },
            ),
        ]

    @server.call_tool()
    async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
        """Handle tool calls from clients"""
        try:
            logger.info(f"Tool call: {name} with arguments {arguments}")
            match name:
                case "search":
                    if "query" not in arguments:
                        return [
                            types.TextContent(
                                type="text", text="Error: Missing required parameter 'query'"
                            )
                        ]
                    include_bibtex = arguments.get("include_bibtex", False)
                    result = search(
                        query=arguments.get("query"),
                        max_results=arguments.get("max_results", 10),
                        year_from=arguments.get("year_from"),
                        year_to=arguments.get("year_to"),
                        venue_filter=arguments.get("venue_filter"),
                        include_bibtex=include_bibtex,
                    )
                    if include_bibtex:
                        return [
                            types.TextContent(
                                type="text",
                                text=f"Found {len(result)} publications matching your query:\n\n{format_results_with_bibtex(result)}",
                            )
                        ]
                    else:
                        return [
                            types.TextContent(
                                type="text",
                                text=f"Found {len(result)} publications matching your query:\n\n{format_results(result)}",
                            )
                        ]
                case "fuzzy_title_search":
                    if "title" not in arguments or "similarity_threshold" not in arguments:
                        return [
                            types.TextContent(
                                type="text",
                                text="Error: Missing required parameter 'title' or 'similarity_threshold'",
                            )
                        ]
                    include_bibtex = arguments.get("include_bibtex", False)
                    result = fuzzy_title_search(
                        title=arguments.get("title"),
                        similarity_threshold=arguments.get("similarity_threshold"),
                        max_results=arguments.get("max_results", 10),
                        year_from=arguments.get("year_from"),
                        year_to=arguments.get("year_to"),
                        venue_filter=arguments.get("venue_filter"),
                        include_bibtex=include_bibtex,
                    )
                    if include_bibtex:
                        return [
                            types.TextContent(
                                type="text",
                                text=f"Found {len(result)} publications with similar titles:\n\n{format_results_with_similarity_and_bibtex(result)}",
                            )
                        ]
                    else:
                        return [
                            types.TextContent(
                                type="text",
                                text=f"Found {len(result)} publications with similar titles:\n\n{format_results_with_similarity(result)}",
                            )
                        ]
                case "get_author_publications":
                    if "author_name" not in arguments or "similarity_threshold" not in arguments:
                        return [
                            types.TextContent(
                                type="text",
                                text="Error: Missing required parameter 'author_name' or 'similarity_threshold'",
                            )
                        ]
                    include_bibtex = arguments.get("include_bibtex", False)
                    result = get_author_publications(
                        author_name=arguments.get("author_name"),
                        similarity_threshold=arguments.get("similarity_threshold"),
                        max_results=arguments.get("max_results", 20),
                        include_bibtex=include_bibtex,
                    )
                    pub_count = result.get("publication_count", 0)
                    publications = result.get("publications", [])

                    if include_bibtex:
                        return [
                            types.TextContent(
                                type="text",
                                text=f"Found {pub_count} publications for author {arguments['author_name']}:\n\n{format_results_with_bibtex(publications)}",
                            )
                        ]
                    else:
                        return [
                            types.TextContent(
                                type="text",
                                text=f"Found {pub_count} publications for author {arguments['author_name']}:\n\n{format_results(publications)}",
                            )
                        ]
                case "get_venue_info":
                    if "venue_name" not in arguments:
                        return [
                            types.TextContent(
                                type="text", text="Error: Missing required parameter 'venue_name'"
                            )
                        ]
                    result = get_venue_info(venue_name=arguments.get("venue_name"))
                    return [
                        types.TextContent(
                            type="text",
                            text=f"Venue information for {arguments['venue_name']}:\n\n{format_dict(result)}",
                        )
                    ]
                case "calculate_statistics":
                    if "results" not in arguments:
                        return [
                            types.TextContent(
                                type="text", text="Error: Missing required parameter 'results'"
                            )
                        ]
                    result = calculate_statistics(results=arguments.get("results"))
                    return [
                        types.TextContent(
                            type="text", text=f"Statistics calculated:\n\n{format_dict(result)}"
                        )
                    ]
                case "export_bibtex":
                    if "links" not in arguments:
                        return [
                            types.TextContent(
                                type="text", text="Error: Missing required parameter 'links'"
                            )
                        ]

                    html_links = arguments.get("links")
                    links = parse_html_links(html_links)

                    if not links:
                        return [
                            types.TextContent(
                                type="text", text="Error: No valid links found in the input"
                            )
                        ]

                    # Fetch and process BibTeX entries
                    bibtex_entries = []
                    for url, key in links:
                        bibtex = fetch_and_process_bibtex(url, key)
                        bibtex_entries.append(bibtex)

                    # Export to file
                    filepath = export_bibtex_entries(bibtex_entries, export_dir)

                    return [
                        types.TextContent(
                            type="text",
                            text=f"Exported {len(bibtex_entries)} BibTeX entries to {filepath}",
                        )
                    ]
                case _:
                    return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
        except Exception as e:
            logger.error(f"Tool execution failed: {str(e)}", exc_info=True)
            return [types.TextContent(type="text", text=f"Error executing {name}: {str(e)}")]

    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="mcp-dblp",
                server_version=version_str,
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )


def format_results(results):
    if not results:
        return "No results found."
    formatted = []
    for i, result in enumerate(results):
        title = result.get("title", "Untitled")
        authors = ", ".join(result.get("authors", []))
        venue = result.get("venue", "Unknown venue")
        year = result.get("year", "")
        formatted.append(f"{i + 1}. {title}")
        formatted.append(f"   Authors: {authors}")
        formatted.append(f"   Venue: {venue} ({year})")
        formatted.append("")
    return "\n".join(formatted)


def format_results_with_similarity(results):
    if not results:
        return "No results found."
    formatted = []
    for i, result in enumerate(results):
        title = result.get("title", "Untitled")
        authors = ", ".join(result.get("authors", []))
        venue = result.get("venue", "Unknown venue")
        year = result.get("year", "")
        similarity = result.get("similarity", 0.0)
        formatted.append(f"{i + 1}. {title} [Similarity: {similarity:.2f}]")
        formatted.append(f"   Authors: {authors}")
        formatted.append(f"   Venue: {venue} ({year})")
        formatted.append("")
    return "\n".join(formatted)


def format_results_with_bibtex(results):
    if not results:
        return "No results found."
    formatted = []
    for i, result in enumerate(results):
        title = result.get("title", "Untitled")
        authors = ", ".join(result.get("authors", []))
        venue = result.get("venue", "Unknown venue")
        year = result.get("year", "")
        formatted.append(f"{i + 1}. {title}")
        formatted.append(f"   Authors: {authors}")
        formatted.append(f"   Venue: {venue} ({year})")
        if "bibtex" in result and result["bibtex"]:
            formatted.append("\n   BibTeX:")
            bibtex_lines = result["bibtex"].strip().split("\n")
            formatted.append("      " + "\n      ".join(bibtex_lines))
        formatted.append("")
    return "\n".join(formatted)


def format_results_with_similarity_and_bibtex(results):
    if not results:
        return "No results found."
    formatted = []
    for i, result in enumerate(results):
        title = result.get("title", "Untitled")
        authors = ", ".join(result.get("authors", []))
        venue = result.get("venue", "Unknown venue")
        year = result.get("year", "")
        similarity = result.get("similarity", 0.0)
        formatted.append(f"{i + 1}. {title} [Similarity: {similarity:.2f}]")
        formatted.append(f"   Authors: {authors}")
        formatted.append(f"   Venue: {venue} ({year})")
        if "bibtex" in result and result["bibtex"]:
            formatted.append("\n   BibTeX:")
            bibtex_lines = result["bibtex"].strip().split("\n")
            formatted.append("      " + "\n      ".join(bibtex_lines))
        formatted.append("")
    return "\n".join(formatted)


def format_dict(data):
    formatted = []
    for key, value in data.items():
        formatted.append(f"{key}: {value}")
    return "\n".join(formatted)


def main() -> int:
    parser = argparse.ArgumentParser(description="MCP-DBLP Server")
    parser.add_argument(
        "--exportdir",
        type=str,
        default=os.path.expanduser("~/.mcp-dblp/exports"),
        help="Directory to export BibTeX files to",
    )
    args = parser.parse_args()

    logger.info(f"Starting MCP-DBLP server with version: {version_str}")
    try:
        asyncio.run(serve(export_dir=args.exportdir))
        return 0
    except KeyboardInterrupt:
        logger.info("Server stopped by user")
        return 0
    except Exception as e:
        logger.error(f"Server error: {str(e)}", exc_info=True)
        return 1


if __name__ == "__main__":
    sys.exit(main())

```

