# Directory Structure
```
├── .gitignore
├── LICENSE
├── main.py
├── README.md
└── requirements.txt
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python
2 | __pycache__/
3 | venv/
4 | .env/
5 | .venv/
6 |
7 | # VS Code
8 | .vscode/
9 |
10 | # macOS
11 | .DS_Store
12 |
13 | # Local env files
14 | .envrc
15 | .env.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # mcp-medium-accelerator
2 |
3 | Questo MCP server permette a LLM come ad esempio Claude Desktop, dando in input una URL tipo "https://medium.com/tag/frontend/archive" di estrapolare tutti i link degli ultimi articoli. Creare un riassunto, anche in italiano degli articoli che interessano all'utente. Salvare il riassunto in una memoria locale da poter interpellare in qualsiasi momento.
4 |
5 | ---
6 |
7 | ## Requisiti
8 |
9 | Assicurati di avere installato:
10 |
11 | - Python ≥ 3.10
12 | - Claude Desktop
13 |
14 | ---
15 |
16 | ## Installazione locale
17 |
18 | 1. Clona il repository:
19 |
20 | ```bash
21 | git clone https://github.com/crtdaniele/mcp-medium-accelerator
22 | cd mcp-medium-accelerator
23 | ```
24 |
25 | 2. Crea e attiva un ambiente virtuale:
26 |
27 | ```bash
28 | python -m venv venv
29 | source venv/bin/activate
30 | ```
31 |
32 | 3. Installa le dipendenze:
33 |
34 | ```bash
35 | pip install -r requirements.txt
36 | ```
37 |
38 | 4. (Facoltativo) Aggiorna il file requirements.txt dopo aver aggiunto nuove librerie:
39 |
40 | ```bash
41 | pip freeze > requirements.txt
42 | ```
43 |
44 | ## Avvio del server MCP
45 |
46 | Per eseguire il server MCP in modalità sviluppo con hot reload:
47 |
48 | ```bash
49 | mcp dev main.py
50 | ```
51 |
52 | Per eseguire il server in modalità normale:
53 |
54 | ```bash
55 | mcp run main.py
56 | ```
57 |
58 | ## Tool
59 |
60 | Tool disponibili:
61 |
62 | - **extract_article_links:**
63 |
64 | Estrae i link degli articoli da un URL di archivio Medium. Restituisce una lista di link agli articoli.
65 |
66 | - **extract_article_text:**
67 |
68 | Estrae il contenuto di un articolo da un URL di Medium. Restituisce il contenuto dell’articolo. Chiede all’utente se desidera salvare il riassunto tramite save_summary.
69 |
70 | - **save_summary:**
71 |
72 | Salva un riassunto di un articolo con titolo, URL e tag. Restituisce un messaggio di stato.
73 |
74 | - **list_summaries:**
75 |
76 | Elenca tutti i riassunti salvati. Restituisce una lista di riassunti.
77 |
78 | ## Installazione su Claude Desktop
79 |
80 | ```bash
81 | mcp install main.py
82 | ```
83 |
84 | Oppure configura manualmente il file settings.json (Claude Desktop > Settings > Advanced):
85 |
86 | ```bash
87 | {
88 | "mcpServers": {
89 | "mcp-medium-accelerator": {
90 | "command": "/opt/homebrew/bin/uv",
91 | "args": [
92 | "run",
93 | "--with",
94 | "bs4",
95 | "--with",
96 | "httpx",
97 | "--with",
98 | "datetime",
99 | "--with",
100 | "tinydb",
101 | "--with",
102 | "mcp[cli]",
103 | "mcp",
104 | "run",
105 | "/your-local-path/main.py"
106 | ]
107 | }
108 | }
109 | }
110 | ```
111 |
112 | ## Licenza
113 |
114 | MIT License.
115 | © 2025 Daniele Carta
116 |
117 | # Contribuire
118 |
119 | Pull request benvenute!
120 |
121 | Segnala bug o richiedi funzionalità aprendo una issue.
122 |
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | annotated-types==0.7.0
2 | anyio==4.9.0
3 | beautifulsoup4==4.13.4
4 | bs4==0.0.2
5 | certifi==2025.4.26
6 | charset-normalizer==3.4.2
7 | click==8.2.1
8 | h11==0.16.0
9 | httpcore==1.0.9
10 | httpx==0.28.1
11 | httpx-sse==0.4.0
12 | idna==3.10
13 | markdown-it-py==3.0.0
14 | mcp==1.9.2
15 | mdurl==0.1.2
16 | pydantic==2.11.5
17 | pydantic-settings==2.9.1
18 | pydantic_core==2.33.2
19 | Pygments==2.19.1
20 | python-dotenv==1.1.0
21 | python-multipart==0.0.20
22 | requests==2.32.3
23 | rich==14.0.0
24 | shellingham==1.5.4
25 | sniffio==1.3.1
26 | soupsieve==2.7
27 | sse-starlette==2.3.5
28 | starlette==0.47.0
29 | tinydb==4.8.2
30 | typer==0.16.0
31 | typing-inspection==0.4.1
32 | typing_extensions==4.13.2
33 | urllib3==2.4.0
34 | uvicorn==0.34.2
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
1 | from mcp.server.fastmcp import FastMCP
2 | import httpx
3 | from bs4 import BeautifulSoup
4 | import os
5 | from tinydb import TinyDB, Query
6 | from datetime import datetime
7 |
8 | mcp = FastMCP("mcp-medium-accelerator")
9 |
10 | home_dir = os.path.expanduser("~")
11 | data_dir = os.path.join(home_dir, "mcp-medium-accelerator_data")
12 | os.makedirs(data_dir, exist_ok=True)
13 |
14 | db_path = os.path.join(data_dir, "summaries.json")
15 | db = TinyDB(db_path)
16 | summaries_table = db.table("summaries")
17 |
18 | @mcp.tool(
19 | description="Extracts article links from a Medium archive URL. Returns a list of article links.",
20 | name="extract_article_links",
21 | )
22 | def extract_article_links(archive_url: str, limit: int = 10) -> list[str]:
23 | response = httpx.get(archive_url)
24 | soup = BeautifulSoup(response.text, "html.parser")
25 | links = []
26 |
27 | for div in soup.find_all("article", attrs={"data-testid": "post-preview"}):
28 | for inner_div in div.find_all("div"):
29 | if inner_div.has_attr("data-href"):
30 | div_with_data_href = inner_div["data-href"]
31 | break
32 | if div_with_data_href:
33 | href = div_with_data_href
34 | if href not in links:
35 | links.append(href)
36 | if len(links) >= limit:
37 | break
38 |
39 | return links
40 |
41 | @mcp.tool(
42 | description="Saves a summary of an article with its title, URL, and tags. Returns a status message.",
43 | name="save_summary",
44 | )
45 | def save_summary(title: str, url: str, summary: str, tags: list[str] = []):
46 | entry = {
47 | "title": title,
48 | "url": url,
49 | "summary": summary,
50 | "tags": tags,
51 | "saved_at": datetime.utcnow().isoformat()
52 | }
53 |
54 | Article = Query()
55 | if summaries_table.contains(Article.url == url):
56 | return {"status": "already_saved"}
57 |
58 | summaries_table.insert(entry)
59 | return {"status": "ok"}
60 |
61 | @mcp.tool(
62 | description="Lists all saved summaries. Returns a list of summaries.",
63 | name="list_summaries",
64 | )
65 | def list_summaries():
66 | return summaries_table.all()
67 |
68 | @mcp.tool(
69 | description="Extracts the text content from a saved article summary. Ask if the user want to save it with save_summary.",
70 | name="extract_article_text",
71 | )
72 | def extract_article_text(url: str):
73 | response = httpx.get(url)
74 | soup = BeautifulSoup(response.text, "html.parser")
75 | article = soup.find("article")
76 | return article.get_text() if article else ""
```