Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """MCP μλ² β μμ΄μ νΈκ° λκ΅¬λ‘ μ§μ μ°Έκ°νλ€. | |
| μ΄κ² μ΄ μ±λ¦°μ§μ μ 체μ±μ΄λ€. Numeraiλ M6λ μ¬λμ΄ CSVλ₯Ό μ¬λ¦°λ€. | |
| μ°Έκ°μλ μ΄ ν μ€λ§ μΉλ©΄ λλ€: | |
| claude mcp add finchal https://<space>/mcp --header "Authorization: Bearer fc_xxx" | |
| κ·Έλ¬λ©΄ μμ΄μ νΈμκ² λꡬ λ€ κ°κ° μκΈ°κ³ , μ¬λμ "μ±λ¦°μ§ μ°Έκ°ν΄μ€" ν λ§λλ§ νλ©΄ λλ€. | |
| νλ‘ν μ½μ JSON-RPC 2.0 μμ Streamable HTTP. μΈ λ©μλλ§ κ΅¬ννλ©΄ λΆλλ€: | |
| initialize / tools/list / tools/call | |
| notifications/* λ μλ΅ μμ΄ 202 λ‘ λ°λλ€. | |
| """ | |
| import json, os, sys | |
| from typing import Any, Dict | |
| sys.path[:0] = [os.path.dirname(os.path.abspath(__file__))] | |
| import i18n as I | |
| PROTOCOL = "2025-06-18" | |
| SERVER = {"name": "finchal", "version": "1.0.0"} | |
| TOOLS = [ | |
| { | |
| "name": "get_rules", | |
| "description": ( | |
| "μ±λ¦°μ§ κ·μΉκ³Ό νμ¬ μμ¦ μ 보λ₯Ό μ½λλ€. μ°Έκ° μ μ λ°λμ λ¨Όμ νΈμΆνλ€. " | |
| "μ’ λͺ© λͺ©λ‘Β·λ§κ°μΌΒ·λ λ²λ¦¬μ§ μνΒ·μμλ£Β·μ±μ λ°©μμ΄ λ€μ΄ μλ€."), | |
| "inputSchema": {"type": "object", "properties": {}}, | |
| }, | |
| { | |
| "name": "get_data", | |
| "description": ( | |
| "μ’ λͺ©μ κ³Όκ±° μμΈλ₯Ό λ°λλ€. μ΄ λ°μ΄ν°λ‘ μ§μ λͺ¨λΈμ λ§λ€μ΄ μμΈ‘νλΌ. " | |
| "λ―Έλ λ°μ΄ν°λ μ£Όμ§ μλλ€ β μ λ΅μ μμ§ μΈμμ μλ€."), | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "asset": {"type": "string", | |
| "description": "μ’ λͺ© μ½λ. NVDA / BTC / GOLD / OIL"}, | |
| "bars": {"type": "integer", "default": 500, | |
| "description": "λ°μ λ΄ κ°μ / number of bars (max 5000)"}, | |
| # π΄ μκ°λ΄μ λͺ©λ‘μ λλ©΄ μ λλ€. μ°λ¦¬ μμΈ νΌλλ μΌλ΄λ§ λ°ννλλ° | |
| # 1h λ₯Ό λ°μ λκ³ μΌλ΄μ λλ €μ£Όλ©΄ μμ΄μ νΈλ μκ°λ΄μΌλ‘ λ―Ώκ³ | |
| # λͺ¨λΈμ μΈμ΄λ€. μ‘°μ©ν νλ¦° λ΅μ΄ κ°μ₯ λμλ€. | |
| "interval": {"type": "string", "enum": ["1d", "1h"], "default": "1d", | |
| "description": "μΌλ΄ λλ μκ°λ΄. μ±μ μ μκ°λ΄ 격μμμ " | |
| "μ΄λ€μ§λλ€ / daily or hourly; scoring runs " | |
| "on the hourly grid"}, | |
| }, | |
| "required": ["asset"], | |
| }, | |
| }, | |
| { | |
| "name": "submit_position", | |
| "description": ( | |
| "ν¬μ§μ μ μ μΆνλ€. -1.0(μ λ μ) ~ +1.0(μ λ λ‘±). λ λ²λ¦¬μ§λ 1λ‘ κ³ μ μ΄λ©° " | |
| "λ²μλ₯Ό λ²μ΄λλ©΄ μλ¦°λ€. λ€μ μ μΆν λκΉμ§ μ΄ ν¬μ§μ μ΄ μ μ§λλ€. " | |
| "ν¬μ§μ μ λ°κΏ λλ§λ€ μμλ£κ° λκ°λ―λ‘ μμ£Ό λ€μ§μΌλ©΄ μν΄λ€."), | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "asset": {"type": "string"}, | |
| "position": {"type": "number", "minimum": -1, "maximum": 1}, | |
| "note": {"type": "string", "description": "μ ν. νλ¨ κ·Όκ±° λ©λͺ¨"}, | |
| }, | |
| "required": ["asset", "position"], | |
| }, | |
| }, | |
| { | |
| "name": "check_score", | |
| "description": ( | |
| "λ΄ μ±μ κ³Ό μμλ₯Ό νμΈνλ€. μμ΅λ₯ Β·μμΒ·μ΄μ νκ³μ λλΉ μμΉλ₯Ό λλ €μ€λ€."), | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": {"asset": {"type": "string", | |
| "description": "μλ΅νλ©΄ μ°Έκ° μ€μΈ μ μ’ λͺ©"}}, | |
| }, | |
| }, | |
| ] | |
| def _ok(rid, result): | |
| return {"jsonrpc": "2.0", "id": rid, "result": result} | |
| def _err(rid, code, msg): | |
| return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": msg}} | |
| def _text(obj): | |
| """λꡬ κ²°κ³Όλ ν μ€νΈ μ½ν μΈ λ‘ λλ €μ€λ€. μμ΄μ νΈκ° JSONμ μ μ½μΌλ―λ‘ | |
| ꡬ쑰λ₯Ό κ·Έλλ‘ μ§λ ¬ννλ, μ¬λμ΄ λ‘κ·Έλ₯Ό λ³Ό λλ μ½νκ² λ€μ¬μ΄λ€.""" | |
| return {"content": [{"type": "text", | |
| "text": json.dumps(obj, ensure_ascii=False, indent=1)}]} | |
| def tools(lang="ko"): | |
| """λꡬ λͺ©λ‘. μ€λͺ μ΄ μΈμ΄λ₯Ό νλ€ β μμ΄μ νΈλ μ΄ μ€λͺ μ μ½κ³ νλ¨νλ€.""" | |
| out = [] | |
| for t in TOOLS: | |
| d = dict(t) | |
| d["description"] = I.t(I.TOOLDESC.get(t["name"], t["description"]), lang) | |
| out.append(d) | |
| return out | |
| def handle(body: Dict[str, Any], call, lang: str = "ko") -> Any: | |
| """call(name, args) -> dict λ₯Ό λ°μ λꡬλ₯Ό μ€μ λ‘ μ€ννλ€. | |
| λ°νμ΄ None μ΄λ©΄ μλ΅ μμ(notification)μ΄λΌλ λ»μ΄λ€. | |
| """ | |
| method = body.get("method") | |
| rid = body.get("id") | |
| if method == "initialize": | |
| return _ok(rid, { | |
| "protocolVersion": PROTOCOL, | |
| "capabilities": {"tools": {"listChanged": False}}, | |
| "serverInfo": SERVER, | |
| "instructions": I.t(I.MCP_INSTRUCTIONS, lang), | |
| }) | |
| if method and method.startswith("notifications/"): | |
| return None # μλ¦Όμ μλ΅νμ§ μλλ€ | |
| if method == "tools/list": | |
| return _ok(rid, {"tools": tools(lang)}) | |
| if method == "tools/call": | |
| p = body.get("params") or {} | |
| name = p.get("name") | |
| args = p.get("arguments") or {} | |
| if not any(t["name"] == name for t in TOOLS): | |
| return _err(rid, -32602, "μ μ μλ λꡬ: %s" % name) | |
| try: | |
| return _ok(rid, _text(call(name, args))) | |
| except PermissionError as e: | |
| return _ok(rid, {"content": [{"type": "text", "text": str(e)}], | |
| "isError": True}) | |
| except Exception as e: | |
| return _ok(rid, {"content": [ | |
| {"type": "text", "text": "%s: %s" % (type(e).__name__, e)}], | |
| "isError": True}) | |
| if method == "ping": | |
| return _ok(rid, {}) | |
| return _err(rid, -32601, "μ§μνμ§ μλ λ©μλ: %s" % method) | |