Spaces:
Running
Running
| """Stateless MCP server (Streamable HTTP, JSON mode) — warehouse stok. | |
| Tek POST endpoint uzerinden JSON-RPC 2.0 konusur. Oturum (session) ve SSE | |
| stream yok: her istek bagimsiz. Bu sayede telefon/web/desktop Claude Code dahil | |
| tum remote MCP client'lardan tek bir URL ile erisilebilir — Python kurulumu veya | |
| lokal stdio process gerekmez. | |
| Veri kaynagi: ayni Space'in cache'lenmis warehouse XML'i (get_cached_warehouse_xml). | |
| Arama mantigi mcp-warehouse/server.py ile birebir ayni tutulur. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import xml.etree.ElementTree as ET | |
| from typing import Any | |
| from stock_service import get_cached_warehouse_xml | |
| PROTOCOL_VERSION = "2025-06-18" | |
| MAX_RESULTS = 100 | |
| TOOLS = [ | |
| { | |
| "name": "search_stock", | |
| "description": ( | |
| "Urun adi, SKU/urun kodu veya barkod ile stok arar. " | |
| "Eslesen urunlerin her depodaki adedini ve toplam stogunu doner. " | |
| 'Ornek sorgular: "GOBIK forma", "10-01-027", "8434738174740".' | |
| ), | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "query": { | |
| "type": "string", | |
| "description": "Aranacak urun adi, SKU/urun kodu veya barkod.", | |
| } | |
| }, | |
| "required": ["query"], | |
| }, | |
| }, | |
| { | |
| "name": "list_warehouse", | |
| "description": ( | |
| "Belirli bir deponun urunlerini ve adetlerini listeler. " | |
| 'warehouse: depo adi (orn. "CADDEBOSTAN", "ALSANCAK", "BAHCEKOY"; ' | |
| "buyuk/kucuk harf onemsiz, kismi eslesir). " | |
| "only_in_stock: True ise sadece stogu 0'dan buyuk urunler." | |
| ), | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "warehouse": { | |
| "type": "string", | |
| "description": "Depo adi (kismi eslesme, buyuk/kucuk harf onemsiz).", | |
| }, | |
| "only_in_stock": { | |
| "type": "boolean", | |
| "description": "True ise sadece stogu 0'dan buyuk urunler.", | |
| "default": False, | |
| }, | |
| }, | |
| "required": ["warehouse"], | |
| }, | |
| }, | |
| ] | |
| # ---------- XML parse + arama (mcp-warehouse/server.py ile ayni) ---------- | |
| def _parse_products(xml: str) -> list[dict]: | |
| if not xml: | |
| return [] | |
| root = ET.fromstring(xml) | |
| products = [] | |
| for p in root.findall("Product"): | |
| warehouses = {} | |
| for w in p.findall("Warehouse"): | |
| name = (w.findtext("Name") or "").strip() | |
| try: | |
| stock = int(w.findtext("Stock") or 0) | |
| except ValueError: | |
| stock = 0 | |
| if name: | |
| warehouses[name] = stock | |
| products.append({ | |
| "code": (p.findtext("ProductCode") or "").strip(), | |
| "name": (p.findtext("ProductName") or "").strip(), | |
| "variant": (p.findtext("ProductVariant") or "").strip(), | |
| "barcode": (p.findtext("Barcode") or "").strip(), | |
| "total_stock": int(p.findtext("TotalStock") or 0), | |
| "warehouses": warehouses, | |
| }) | |
| return products | |
| def _search_stock(query: str, products: list[dict]) -> dict: | |
| q = query.strip().lower() | |
| if not q: | |
| return {"error": "Bos sorgu", "results": []} | |
| matches = [] | |
| for p in products: | |
| haystack = f"{p['name']} {p['code']} {p['barcode']} {p['variant']}".lower() | |
| if q in haystack: | |
| matches.append(p) | |
| total = len(matches) | |
| return { | |
| "query": query, | |
| "match_count": total, | |
| "truncated": total > MAX_RESULTS, | |
| "results": matches[:MAX_RESULTS], | |
| } | |
| def _list_warehouse(warehouse: str, only_in_stock: bool, products: list[dict]) -> dict: | |
| wq = warehouse.strip().lower() | |
| if not wq: | |
| return {"error": "Bos depo adi", "results": []} | |
| results = [] | |
| for p in products: | |
| for wname, stock in p["warehouses"].items(): | |
| if wq in wname.lower(): | |
| if only_in_stock and stock <= 0: | |
| continue | |
| results.append({ | |
| "code": p["code"], | |
| "name": p["name"], | |
| "variant": p["variant"], | |
| "warehouse": wname, | |
| "stock": stock, | |
| }) | |
| total = len(results) | |
| return { | |
| "warehouse": warehouse, | |
| "match_count": total, | |
| "truncated": total > MAX_RESULTS, | |
| "results": results[:MAX_RESULTS], | |
| } | |
| async def _run_tool(name: str, args: dict) -> dict: | |
| xml = await asyncio.to_thread(get_cached_warehouse_xml) | |
| products = _parse_products(xml or "") | |
| if name == "search_stock": | |
| return _search_stock(str(args.get("query", "")), products) | |
| if name == "list_warehouse": | |
| return _list_warehouse( | |
| str(args.get("warehouse", "")), | |
| bool(args.get("only_in_stock", False)), | |
| products, | |
| ) | |
| raise ValueError(f"Bilinmeyen arac: {name}") | |
| # ---------- JSON-RPC 2.0 dispatch ---------- | |
| def _result(id_: Any, result: dict) -> dict: | |
| return {"jsonrpc": "2.0", "id": id_, "result": result} | |
| def _error(id_: Any, code: int, message: str) -> dict: | |
| return {"jsonrpc": "2.0", "id": id_, "error": {"code": code, "message": message}} | |
| async def handle_message(msg: dict) -> dict | None: | |
| """Tek bir JSON-RPC mesajini isler. Notification ise None doner (HTTP 202).""" | |
| if not isinstance(msg, dict): | |
| return _error(None, -32600, "Invalid Request") | |
| method = msg.get("method") | |
| id_ = msg.get("id") | |
| if method == "initialize": | |
| client_ver = (msg.get("params") or {}).get("protocolVersion") or PROTOCOL_VERSION | |
| return _result(id_, { | |
| "protocolVersion": client_ver, | |
| "capabilities": {"tools": {"listChanged": False}}, | |
| "serverInfo": {"name": "warehouse", "version": "1.0.0"}, | |
| }) | |
| if method == "ping": | |
| return _result(id_, {}) | |
| if method == "tools/list": | |
| return _result(id_, {"tools": TOOLS}) | |
| if method == "tools/call": | |
| params = msg.get("params") or {} | |
| name = params.get("name") | |
| args = params.get("arguments") or {} | |
| try: | |
| data = await _run_tool(name, args) | |
| except Exception as e: # noqa: BLE001 | |
| return _result(id_, { | |
| "content": [{"type": "text", "text": f"Hata: {e}"}], | |
| "isError": True, | |
| }) | |
| return _result(id_, { | |
| "content": [{"type": "text", "text": json.dumps(data, ensure_ascii=False)}], | |
| "isError": False, | |
| }) | |
| # notifications/* ve diger bildirimlerin id'si yoktur -> cevap verme | |
| if id_ is None: | |
| return None | |
| return _error(id_, -32601, f"Method not found: {method}") | |