diff --git a/.gitignore b/.gitignore index 2b5f55c..fc485e2 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ htmlcov/ *.log .DS_Store + +# Runtime artifacts +*.whl +phoenix/data/ diff --git a/mcp-stub/Dockerfile b/mcp-stub/Dockerfile new file mode 100644 index 0000000..5ab3c8b --- /dev/null +++ b/mcp-stub/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY ./PySocks-1.7.1-py3-none-any.whl /app/PySocks-1.7.1-py3-none-any.whl +RUN pip install --no-cache-dir /app/PySocks-1.7.1-py3-none-any.whl + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY app.py /app/app.py + +EXPOSE 8081 + +CMD ["python", "app.py"] diff --git a/mcp-stub/README.md b/mcp-stub/README.md new file mode 100644 index 0000000..8e45883 --- /dev/null +++ b/mcp-stub/README.md @@ -0,0 +1,39 @@ +# MCP Stub Service + +Минимальный MCP server (Streamable HTTP) для локальной интеграции. + +## Запуск + +```bash +docker compose up --build +``` + +Сервис поднимется на `http://localhost:8081`. +MCP endpoint: `http://localhost:8081/mcp`. + +## Инструменты MCP + +- `search_news_sources(url: str)` +- `parse_article(url: str)` +- `extract_publication_date(article_text: str)` +- `rank_sources_by_date(items: list[dict])` +- `generate_summary(items: list[dict])` + +Пример проверки через Python MCP client: + +```bash +python - <<'PY' +import asyncio +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async def main(): + async with streamablehttp_client(url="http://localhost:8081/mcp") as (read, write, *_): + async with ClientSession(read, write) as session: + await session.initialize() + result = await session.call_tool("search_news_sources", {"url": "https://example.com/news"}) + print(result.structuredContent) + +asyncio.run(main()) +PY +``` diff --git a/mcp-stub/app.py b/mcp-stub/app.py new file mode 100644 index 0000000..59bb456 --- /dev/null +++ b/mcp-stub/app.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import os +from datetime import datetime, timezone +from typing import Any + +from mcp.server.fastmcp import FastMCP + + +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _base_result(tool_name: str, ok: bool, payload: dict[str, Any]) -> dict[str, Any]: + return { + "tool_name": tool_name, + "ok": ok, + "payload": payload, + "received_at": _utc_now_iso(), + } + + +mcp = FastMCP( + "prisma_mcp_stub", + host=os.getenv("MCP_STUB_HOST", "0.0.0.0"), + port=int(os.getenv("MCP_STUB_PORT", "8081")), + streamable_http_path="/mcp", +) + + +@mcp.tool() +def search_news_sources(url: str) -> dict[str, Any]: + return _base_result( + tool_name="search_news_sources", + ok=True, + payload={ + "input_url": str(url), + "items": [ + {"url": "https://news-a.example/article-1"}, + {"url": "https://news-b.example/article-2"}, + {"url": "https://news-c.example/article-3"}, + ], + }, + ) + + +@mcp.tool() +def parse_article(url: str) -> dict[str, Any]: + return _base_result( + tool_name="parse_article", + ok=True, + payload={ + "url": str(url), + "title": "Stub article title", + "published_at": "2026-01-01T10:00:00+00:00", + "text": "Stub parsed article content.", + }, + ) + + +@mcp.tool() +def extract_publication_date(article_text: str) -> dict[str, Any]: + return _base_result( + tool_name="extract_publication_date", + ok=True, + payload={ + "text_size": len(article_text), + "published_at": "2026-01-01T10:00:00+00:00", + "confidence": 0.77, + }, + ) + + +@mcp.tool() +def rank_sources_by_date(items: list[dict[str, Any]]) -> dict[str, Any]: + ranked = sorted(items, key=lambda item: str(item.get("published_at", ""))) + return _base_result( + tool_name="rank_sources_by_date", + ok=True, + payload={ + "input_count": len(items), + "ranked_items": ranked, + }, + ) + + +@mcp.tool() +def generate_summary(items: list[dict[str, Any]]) -> dict[str, Any]: + first_url = "" + if items: + first_url = str(items[0].get("url", "")) + + return _base_result( + tool_name="generate_summary", + ok=True, + payload={ + "input_count": len(items), + "summary": "По заглушечным данным самым ранним источником считается " + first_url, + }, + ) + + +if __name__ == "__main__": + mcp.run(transport="streamable-http") diff --git a/mcp-stub/docker-compose.yml b/mcp-stub/docker-compose.yml new file mode 100644 index 0000000..32cfa74 --- /dev/null +++ b/mcp-stub/docker-compose.yml @@ -0,0 +1,9 @@ +services: + mcp-stub: + build: + context: . + dockerfile: Dockerfile + image: local-mcp-stub:latest + restart: unless-stopped + ports: + - "8081:8081" diff --git a/mcp-stub/requirements.txt b/mcp-stub/requirements.txt new file mode 100644 index 0000000..6664c6d --- /dev/null +++ b/mcp-stub/requirements.txt @@ -0,0 +1 @@ +mcp diff --git a/phoenix/Dockerfile b/phoenix/Dockerfile new file mode 100644 index 0000000..76c633e --- /dev/null +++ b/phoenix/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PHOENIX_WORKING_DIR=/mnt/data +ENV PHOENIX_HOST=0.0.0.0 +ENV PHOENIX_PORT=6006 +ENV PHOENIX_GRPC_PORT=4317 + +COPY ./PySocks-1.7.1-py3-none-any.whl /app/PySocks-1.7.1-py3-none-any.whl +RUN pip install --no-cache-dir /app/PySocks-1.7.1-py3-none-any.whl + +RUN pip install --no-cache-dir arize-phoenix + +WORKDIR /app + +RUN mkdir -p /mnt/data +VOLUME ["/mnt/data"] + +EXPOSE 6006 4317 + +CMD ["phoenix", "serve"] diff --git a/phoenix/docker-compose.yml b/phoenix/docker-compose.yml new file mode 100644 index 0000000..3472252 --- /dev/null +++ b/phoenix/docker-compose.yml @@ -0,0 +1,16 @@ +services: + phoenix: + build: + context: . + dockerfile: Dockerfile + image: local-phoenix:latest + restart: unless-stopped + ports: + - "6006:6006" + - "4317:4317" + environment: + PHOENIX_WORKING_DIR: /mnt/data + # Optional: use external Postgres instead of local SQLite + # PHOENIX_SQL_DATABASE_URL: postgresql://user:password@host:5432/dbname + volumes: + - ./phoenix-data:/mnt/data