From 907e5c5732c0d559e3a69614f5547578050b43b7 Mon Sep 17 00:00:00 2001 From: Corneliu Date: Tue, 14 Apr 2026 14:50:48 +0300 Subject: [PATCH 1/3] Add figshare-mcp: MCP server for Figshare API v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes 9 semantic tools over stdio transport: search_articles, get_article, manage_article, search_collections, get_collection, manage_collection, get_projects, get_account_info, manage_embargo - Auth via FIGSHARE_TOKEN env var (personal access token) - Configurable base URL via FIGSHARE_BASE_URL - No publish/delete operations — drafts only - Hard cap of 1000 results, max 50 per page - Auto-upgrades http:// to https:// for non-local URLs - Integration tests against local/stage instance (47/47 pass) Co-Authored-By: Claude Sonnet 4.6 --- mcp/README.md | 111 ++++++++ mcp/figshare_mcp/__init__.py | 3 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 309 bytes .../__pycache__/client.cpython-314.pyc | Bin 0 -> 10023 bytes .../__pycache__/server.cpython-314.pyc | Bin 0 -> 2635 bytes mcp/figshare_mcp/client.py | 135 +++++++++ mcp/figshare_mcp/server.py | 49 ++++ mcp/figshare_mcp/tools/__init__.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 248 bytes .../tools/__pycache__/account.cpython-314.pyc | Bin 0 -> 3334 bytes .../__pycache__/articles.cpython-314.pyc | Bin 0 -> 12239 bytes .../__pycache__/collections.cpython-314.pyc | Bin 0 -> 10017 bytes .../tools/__pycache__/embargo.cpython-314.pyc | Bin 0 -> 6276 bytes .../__pycache__/projects.cpython-314.pyc | Bin 0 -> 4434 bytes mcp/figshare_mcp/tools/account.py | 69 +++++ mcp/figshare_mcp/tools/articles.py | 264 ++++++++++++++++++ mcp/figshare_mcp/tools/collections.py | 226 +++++++++++++++ mcp/figshare_mcp/tools/embargo.py | 124 ++++++++ mcp/figshare_mcp/tools/projects.py | 103 +++++++ mcp/pyproject.toml | 28 ++ mcp/tests/__init__.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 293 bytes .../conftest.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 2493 bytes .../test_account.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 14186 bytes ...test_articles.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 27912 bytes ...t_collections.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 17804 bytes .../test_embargo.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 18965 bytes ...test_projects.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 9489 bytes mcp/tests/conftest.py | 37 +++ mcp/tests/test_account.py | 77 +++++ mcp/tests/test_articles.py | 147 ++++++++++ mcp/tests/test_collections.py | 98 +++++++ mcp/tests/test_embargo.py | 120 ++++++++ mcp/tests/test_projects.py | 46 +++ 34 files changed, 1639 insertions(+) create mode 100644 mcp/README.md create mode 100644 mcp/figshare_mcp/__init__.py create mode 100644 mcp/figshare_mcp/__pycache__/__init__.cpython-314.pyc create mode 100644 mcp/figshare_mcp/__pycache__/client.cpython-314.pyc create mode 100644 mcp/figshare_mcp/__pycache__/server.cpython-314.pyc create mode 100644 mcp/figshare_mcp/client.py create mode 100644 mcp/figshare_mcp/server.py create mode 100644 mcp/figshare_mcp/tools/__init__.py create mode 100644 mcp/figshare_mcp/tools/__pycache__/__init__.cpython-314.pyc create mode 100644 mcp/figshare_mcp/tools/__pycache__/account.cpython-314.pyc create mode 100644 mcp/figshare_mcp/tools/__pycache__/articles.cpython-314.pyc create mode 100644 mcp/figshare_mcp/tools/__pycache__/collections.cpython-314.pyc create mode 100644 mcp/figshare_mcp/tools/__pycache__/embargo.cpython-314.pyc create mode 100644 mcp/figshare_mcp/tools/__pycache__/projects.cpython-314.pyc create mode 100644 mcp/figshare_mcp/tools/account.py create mode 100644 mcp/figshare_mcp/tools/articles.py create mode 100644 mcp/figshare_mcp/tools/collections.py create mode 100644 mcp/figshare_mcp/tools/embargo.py create mode 100644 mcp/figshare_mcp/tools/projects.py create mode 100644 mcp/pyproject.toml create mode 100644 mcp/tests/__init__.py create mode 100644 mcp/tests/__pycache__/__init__.cpython-314.pyc create mode 100644 mcp/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc create mode 100644 mcp/tests/__pycache__/test_account.cpython-314-pytest-9.0.3.pyc create mode 100644 mcp/tests/__pycache__/test_articles.cpython-314-pytest-9.0.3.pyc create mode 100644 mcp/tests/__pycache__/test_collections.cpython-314-pytest-9.0.3.pyc create mode 100644 mcp/tests/__pycache__/test_embargo.cpython-314-pytest-9.0.3.pyc create mode 100644 mcp/tests/__pycache__/test_projects.cpython-314-pytest-9.0.3.pyc create mode 100644 mcp/tests/conftest.py create mode 100644 mcp/tests/test_account.py create mode 100644 mcp/tests/test_articles.py create mode 100644 mcp/tests/test_collections.py create mode 100644 mcp/tests/test_embargo.py create mode 100644 mcp/tests/test_projects.py diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 00000000..6e6e1ac0 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,111 @@ +# figshare-mcp + +An MCP (Model Context Protocol) server that exposes the [Figshare API v2](https://docs.figshare.com) as tools for Claude and other MCP-compatible AI assistants. + +## What it does + +Provides 9 semantic tools for interacting with any Figshare instance: + +| Tool | Description | +|------|-------------| +| `search_articles` | Search public or private articles | +| `get_article` | Get article details, files, and version history | +| `manage_article` | Create or update a draft article (never publishes) | +| `search_collections` | Search public or private collections | +| `get_collection` | Get collection details and its articles | +| `manage_collection` | Create or update a collection | +| `get_projects` | List or inspect a project | +| `get_account_info` | Fetch profile, available licenses, and subject categories | +| `manage_embargo` | Get, set, or remove an embargo on an article | + +**This MCP never publishes or deletes articles/collections.** Destructive and publish operations must be done via the Figshare web interface. + +## Requirements + +- Python 3.11+ +- Claude Desktop, Claude Code, or any other MCP-compatible client + +## Installation + +Clone the repo and install the package: + +```bash +git clone https://github.com/digital-science/figshare-user-documentation.git +cd figshare-user-documentation/mcp +pip install -e . +``` + +## Configuration + +### 1. Get a personal access token + +In Figshare, go to **Account → Applications → Create personal token**. + +You only need a token for private/authenticated operations. Public search and read work without a token. + +### 2. Add to Claude Desktop + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "figshare": { + "command": "python3", + "args": ["-m", "figshare_mcp.server"], + "env": { + "FIGSHARE_TOKEN": "your-token-here", + "FIGSHARE_BASE_URL": "https://api.figshare.com/v2" + } + } + } +} +``` + +For an institutional instance, replace `FIGSHARE_BASE_URL` with your institution's API base URL. + +### 3. Add to Claude Code + +```bash +claude mcp add figshare \ + -e FIGSHARE_TOKEN=your-token-here \ + -e FIGSHARE_BASE_URL=https://api.figshare.com/v2 \ + -- python3 -m figshare_mcp.server +``` + +## Usage examples + +Once connected, you can ask Claude things like: + +- *"Search for articles about climate change published after 2022"* +- *"Get the details and files for article 12345678"* +- *"Create a new draft article titled 'My Dataset' with tags 'climate' and 'ocean'"* +- *"Show me my private articles"* +- *"What embargo options does my institution have?"* +- *"Set an embargo on article 123 until 2026-06-01"* + +## Running tests + +Integration tests run against a local Figshare instance. + +```bash +# Public tests only (no token needed, uses figshare.com): +pytest tests/ -v -m "not requires_token and not write" + +# All tests against a local instance: +FIGSHARE_TOKEN=xxx \ +FIGSHARE_BASE_URL=http://localhost:8080/v2 \ +pytest tests/ -v + +# Skip write tests (read-only against local instance): +FIGSHARE_TOKEN=xxx \ +FIGSHARE_BASE_URL=http://localhost:8080/v2 \ +pytest tests/ -v -m "not write" +``` + +## Environment variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `FIGSHARE_TOKEN` | For private/write ops | — | Personal access token | +| `FIGSHARE_BASE_URL` | No | `https://api.figshare.com/v2` | API base URL | diff --git a/mcp/figshare_mcp/__init__.py b/mcp/figshare_mcp/__init__.py new file mode 100644 index 00000000..eba56fbd --- /dev/null +++ b/mcp/figshare_mcp/__init__.py @@ -0,0 +1,3 @@ +"""Figshare MCP server — exposes Figshare API v2 as MCP tools.""" + +__version__ = "0.1.0" diff --git a/mcp/figshare_mcp/__pycache__/__init__.cpython-314.pyc b/mcp/figshare_mcp/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3af283a1956eb05e3a409d2c4bb0ac200a90d87d GIT binary patch literal 309 zcmdPq^Md_YD6Ll8p=Ll9#LV-S-vgC=vSEl|)cGrc$? zu_#r+*Ev9;IJKxOwMgMn!xV+oih}&&)M5p=ykmf;LYa|5Vlh}{Nq&A#v0fFcfu5nB zfuAPRE%x~Ml>FrQ_*>lZ@jx?*GxPJ}<5x0#207qXntmwI=wkik{GzgOgGz~$pXocQ?6yv&mL uc)fzkTO2mI`6;D2sdh!|Kx;vsFXjajAD9^#8E-N;Kj0H>;x1wZiU0r)om-dy literal 0 HcmV?d00001 diff --git a/mcp/figshare_mcp/__pycache__/client.cpython-314.pyc b/mcp/figshare_mcp/__pycache__/client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55b61f70a03c6509f775d4989baf29092454676e GIT binary patch literal 10023 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 str: + """Produce a human-readable error string from an HTTP error response.""" + template = _ERROR_MESSAGES.get(status_code, f"Unexpected error (HTTP {status_code})") + # Try to extract a detail message from the response body. + detail = "" + if response_body: + detail = ( + response_body.get("message") + or response_body.get("detail") + or response_body.get("error") + or str(response_body) + ) + return template.format(detail=detail) if "{detail}" in template else template + + +class FigshareClient: + """Thin async wrapper around httpx for Figshare API v2.""" + + def __init__(self) -> None: + raw_url = os.getenv("FIGSHARE_BASE_URL", DEFAULT_BASE_URL).rstrip("/") + self.base_url = self._normalize_base_url(raw_url) + token = os.getenv("FIGSHARE_TOKEN", "") + # Auth header only set when a token is present — public endpoints work without it. + self._headers: dict[str, str] = {"Content-Type": "application/json"} + if token: + self._headers["Authorization"] = f"token {token}" + + @staticmethod + def _normalize_base_url(url: str) -> str: + """Upgrade http:// to https:// for non-local URLs. + + Remote Figshare instances redirect http→https, but the 301 Location header + sometimes omits the 'api.' subdomain, resulting in silent 404s. Upgrading + the scheme upfront avoids the redirect entirely. + """ + if url.startswith("http://") and not any( + url.startswith(f"http://{h}") for h in ("localhost", "127.0.0.1", "0.0.0.0") + ): + return "https://" + url[len("http://"):] + return url + + @property + def has_token(self) -> bool: + return "Authorization" in self._headers + + async def _request( + self, + method: str, + path: str, + params: dict | None = None, + json: dict | None = None, + ) -> Any: + """Execute an HTTP request and return the parsed JSON body. + + Raises RuntimeError with a human-readable message on non-2xx responses. + """ + url = f"{self.base_url}{path}" + # Strip None values from query params so we don't send ?foo=None. + clean_params = {k: v for k, v in (params or {}).items() if v is not None} + + async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as http: + response = await http.request( + method=method, + url=url, + headers=self._headers, + params=clean_params or None, + json=json, + ) + + if response.is_success: + # Handle empty bodies (204 No Content, 205 Reset Content, or any 2xx with no body). + if not response.content: + return {} + return response.json() + + # Parse error body if available. + body = None + try: + body = response.json() + except Exception: + pass + + raise RuntimeError(_build_error_message(response.status_code, body)) + + async def get(self, path: str, params: dict | None = None) -> Any: + return await self._request("GET", path, params=params) + + async def post(self, path: str, json: dict | None = None, params: dict | None = None) -> Any: + return await self._request("POST", path, params=params, json=json) + + async def put(self, path: str, json: dict | None = None) -> Any: + return await self._request("PUT", path, json=json) + + async def patch(self, path: str, json: dict | None = None) -> Any: + return await self._request("PATCH", path, json=json) + + async def delete(self, path: str) -> Any: + return await self._request("DELETE", path) + + +def clamp_page_size(page_size: int) -> int: + """Clamp page_size to the allowed range [1, MAX_PAGE_SIZE].""" + return max(1, min(page_size, MAX_PAGE_SIZE)) diff --git a/mcp/figshare_mcp/server.py b/mcp/figshare_mcp/server.py new file mode 100644 index 00000000..ee7a3a61 --- /dev/null +++ b/mcp/figshare_mcp/server.py @@ -0,0 +1,49 @@ +""" +Figshare MCP server entry point. + +Exposes 9 semantic tools over the MCP stdio transport: + search_articles — search public or private articles + get_article — get article details, files, and versions + manage_article — create or update a draft article (no publish) + search_collections — search public or private collections + get_collection — get collection details and its articles + manage_collection — create or update a collection (no publish) + get_projects — list or get details of a project + get_account_info — profile, licenses, categories, embargo options + manage_embargo — get/set/remove embargo on an article + +Configuration via environment variables: + FIGSHARE_TOKEN — personal access token (required for private/write operations) + FIGSHARE_BASE_URL — API base URL (default: https://api.figshare.com/v2) +""" + +from mcp.server.fastmcp import FastMCP + +from figshare_mcp.tools.account import get_account_info +from figshare_mcp.tools.articles import get_article, manage_article, search_articles +from figshare_mcp.tools.collections import get_collection, manage_collection, search_collections +from figshare_mcp.tools.embargo import manage_embargo +from figshare_mcp.tools.projects import get_projects + +# Create the MCP server instance. +mcp = FastMCP("figshare") + +# Register all tools — FastMCP reads the function signature and docstring automatically. +mcp.tool()(search_articles) +mcp.tool()(get_article) +mcp.tool()(manage_article) +mcp.tool()(search_collections) +mcp.tool()(get_collection) +mcp.tool()(manage_collection) +mcp.tool()(get_projects) +mcp.tool()(get_account_info) +mcp.tool()(manage_embargo) + + +def main() -> None: + """Start the MCP server using stdio transport.""" + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/mcp/figshare_mcp/tools/__init__.py b/mcp/figshare_mcp/tools/__init__.py new file mode 100644 index 00000000..bca3cb59 --- /dev/null +++ b/mcp/figshare_mcp/tools/__init__.py @@ -0,0 +1 @@ +"""Figshare MCP tool modules.""" diff --git a/mcp/figshare_mcp/tools/__pycache__/__init__.cpython-314.pyc b/mcp/figshare_mcp/tools/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff9f0eb683ba45fcc6b17c49cb23e80503ba15f3 GIT binary patch literal 248 zcmdPq^MASDe9K@24fL5#`_noLzvZkg%D8Hq)y3ck(( z3MKjZISRS?DWy57#d?04jJMe1<5TjJA^Qxv&LkO8M@4}EXBOMTRJ z?E!c8?VC5hH}iYL_og=*2@+_D#9!2Z1_=2Zet3;{g`Iv7=1GBA;6MmdXvw{O&U ziMRMRQV(RS z3e(hDmQ`lW?35drjHWBHmMO8>tih`Ko`D+7%9M1aRtCPDfMv@{wK1`X9~fsd>jtjB zdfN^ci*lu+gFP!2jVLT20*^Ta-8^|lPIJB6*lji++2Y~-#T$t&0p3qVawNGWp%fs; zJGPoo3KFqtI|R}W%qEH-d$$Sj{xKo`o%0Z*4$=nT6J9jQTdq;npCwfR9&wy>ySmah zF{b1GYmNB01Mu=}rX{o~aN}aZcM|khl1Fg{`aIAI{9CTqcj3=)1J-hjo98bJK{77X z=ZZHTW2F z{jc$g5l`Dr-P!!OOK?o@`|7GPC58Y{^3uGs12zmS<&ElP5Lyf~bwk1RGX0llBDPGYT#z}W9{bp{b-F?Sn@5sqL*(Yv{{?H!4%8m z1}flwumeS6KjNM;nbDxMYK>y4=oP35HN(mD&)ItrP6*T*m)-ZSe{hvTwL$uSs9G}) ztH>8Kku^jgY=X&#_?_R%jv5+f{eeiznDuv_Va$N$h=@he+87~~RnQn?>g&**si#>~I0@Lu#4jaDownd}aG@n0mz)O0{y;Oa%&}LG(4mRR$V)O@K$BCf!Zm^csKYGs^-&^BzkNu zlAO2G|NzL&oLy|wh)>s@az zhzqw@R8w00*m;QBn<(#rMP+IRREr=^EJ*)8) zPK<6GZNGDCHPHG+DE70VpAIcd{ZjdJD81rOZ=WG2E^=RO9%_3^xJYKRtNT~{-AlOU zC-L@wZSkP{dOJuuE_}5aI|?RE>CN`8kFVct#brMUA4ij>G@5h`15DkfZ?p5{$L8JB zxb!Ds>e%#sy&WeR)^Ihr*_XY~e+sT(nNQ*=$JgM$%@FYZqn><{`&5MPH;0EWkVTOj zKIdPQB$T5Ps25ufk93p8UT)-sf3Z(O`AtWE_HZsn9>lp^#Q)$m3FTJQKcKH+KY)-A z&K%C2A`c^6ZqWZQDxutrHV=~&`*R$4d3Yf-(!nnUqa#UvsZ{{|Qc^%o2aoaz4m>V( zhjKxFX&{;t_+^4N%Yp!!Mkna-HfA1!4&ugf+dhHw??ed~{yXV^Cc;h9xqLda{{vg0Q=R|- literal 0 HcmV?d00001 diff --git a/mcp/figshare_mcp/tools/__pycache__/articles.cpython-314.pyc b/mcp/figshare_mcp/tools/__pycache__/articles.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3674ede2b310c22330ef74cb86cd1ae2990c7ee6 GIT binary patch literal 12239 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%0D*gH1`!|Da2c z@pU%L-_khLLS1*$L(Tf@HD-)AnIQjqN8_-bx;{t`>-5*RnjwCJX@UG3?KITAvC1~& zWp4DcL$%CJI}P!h4g-|ltTkZV%V4~j25mQ6Y(v|bn_bSKLFVQrA~R^fob4!cOGks| zTYB4Y7jw(y9PVIlwW7?e4g=Q`igCQNlCRm ze{mgS<%p})?!_J{&k{{l2UrD(Jwe`Fr5~V#{5(P4Qqj7k;T zitRWnb%G8+l`a`o8e_y^KsJzl0(zb$8`?#LB-u^{95TU&0lH%(6I?3b(;_d~t6ggX z@G5VWG(yTfREF@{AY_1=O=lpYtkFCvR8_Ba%HTCZve9tmJ^s$>Q zJ4l`rpzOPy=Z?;Ie(Y!~yGWj!a@NdkJO|DOlB}gX4fFctfQ=s+1$D2ay>~l zsKQ>7Y@}R3zRsN}H<4sB;$t61Wq_>NUEwVerhgX!@)cMuD*|M&xBo8yVv1{f7FK5h zjwtX+JRMFO5GHfR^-Kl2tp?9A$87mFXjw&oy9QV%WlzV`wVIY_(-Uo^PZ4_Jn`*IC zYYBo6BBE0~0ey;ZgHh7kWEdRLE9t!)fp`&vl0L>c5b|lhlquPh5HTs`-{D-O+^nR} zac+`UeJ6jx)sVF6iTGQtmZWWp{6BFXOas+WM-EPPpgi{FhzC~}aa1h#Korc2$cOW( zU#)@ja*bRQ*Bo&MnF@I4d=+m1w5-8%=$P_wEfE^&(UaPT`yBQIF11GGSFNl12OjIU z>l^mF75eR3so&sFcnkiA;J*hTJ#Js*(TIursjbjsh<(Tt*1Ri~nMAx9=B#_W8;A+{ ztq>Ce{M++JnBY5By7{ye!Uw<>PAs8-!z*3@0ImSf!IyxyyAY$}H6idT03y_^-XbxC zCjr^x{R#Q1l^Em`NwBLEX9F0#!x>e)mz_lS1VP|hA5Qb*fQ`YnJrRrZi`fS`1;98i z>+Qg*%5AO~^y9SLLND7PU+f~~*fcKO8k8b}tmZPx>n7OK;PPPQ(*_Nu zs-^%NMv@?N5^Pp(K~_XAf&e8^OYvTaHiD0?8IIM2S(fj3n;;9~>=fsXHBx8CSTCLm*A{f6(z%__$*DkSGI zvG=17i9mMQDjrRyC&(Nid@yzjZu7;)frct|Mdb~v4{~5}ng>Dg0pPtp*_WYs4?18L zOc0j>##BE7#CEXApP1zFa-HI!kjFwr8(_1;TMRf9&;aPn#FOA)lS!yD zgV3Y#RF4O;R(UEVg7R#vy2d2mio}nCQy;DJ-~bUt@D-@ca`Tg!rLwG`o_a}A?wbfU zc(RK~M!8_xJ(@lhOC_OkvO3wW>6Weli>`?-S+Wbe(IrcF1*ZLExh(RwELn7Fy?xVx z${KxQ1w}bQhC&3k$t8$TIE;C4_)`P~3P&(@6oToR$2hXE3GyYAn2*O{#KOWMl2k@o zrl-hxg}o$SZaLxC35Z;UV-WZ)%d+9^SWa9cOg5jZkYn(MU=uPq#*3!N_)n#YQ+V+Hdukeh3~Yij!Gnu=iO{zCJC zqU)uC`6ab@{=}WO{wj%Zq4`kJ6)Bh_Wh2$nHZxQ*+3%PdOO36whtE1nrrJBEwo;pa z_Sk~iS2Ee|n7k#gZ#Ht)UNY6(F}0RjyJp80%*`c}^Ny*x)Vyl;$XVBtgy)W_z0|&L z_LT*5OUYz?>&;R_)9j(Mwk2Y&J0@Srw|e&I0x)>J4J-n-nz?V^wY8RftMD3Gp$mN@ zTZ_KXz538>NOen`x!ifheAR#LNZ~-F(02H4EOFPv7wkN|j;HB-&pSPZ_5rjO6k4CZ z7M|-V*6zAz-&L^h`V?F~yDsc1__to&b@jPI*Un76V9r=rmy5!$90`lc>-aFbSzI(0nV@K>*jEf$8nT$IpbrA-|2pRq7lM+q< literal 0 HcmV?d00001 diff --git a/mcp/figshare_mcp/tools/__pycache__/collections.cpython-314.pyc b/mcp/figshare_mcp/tools/__pycache__/collections.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f167066072ed4209c8c0252236773b03864d8102 GIT binary patch literal 10017 zcmcgyU2GfIm7dX%oZ-JDO4KikQ6OY2kk>1_8|}Zkl3^XO4$e}FT$;0k|ymRio=iYnn@A=NTv%}|gQSdmL|5KW7r>OtJi1{!Eaql%C7AS$r zQLj+~Jxu3lcvlTq9c6M%JEk$iwxjl({iq}7Xr~01U~8ZRdjplKPSH75a0?EIvqqc~ zJOT%CrxAAw^j0eD&Hpzv;yQU~jL&5K% z#avthq2lQzFXiAZ=cGhRRG^l*csf2KR#b$wBxDg=gvRsPBvNrcDaWUCmae!)!nDR5 zOkW6BX+E>7hfG>P zwNfwAv$Q}@K#u+hRTEVunwhYbXq$z$TlgIonzit7J*-)lg4wfhRpz)Jnd5$BZJu(O zh#ObK*C?}Q|7l*7FKDbWf0_-)Lvxl!MYCt*q$q2C@|sLavc5<(x0F_LQZA1xW<>Lo zm7vUuNf^s?LezY7nWQu=mE!gpIg`(74w(A9oDMS@qvT}GHU;ZUW2AHr=E(u;aW0-y za1J`OOyng+l$A&#Bd5ibl#ht%vr1&zSgyUPOd_7@O^RnDvY5?SGV)NWH6Y@+r z%$zaKq{{CtcsRNvwAr__plfV2H{4uvs{pgYt0h2f8ov1VE-9}4h5A_3P+xMi+(3E~F#Zo8E^VEdL%yUr59vu6u z?};+GCj4fpai1wqc|P)0AsE%H?|=_xhP)+OqV_Q%N}{3FfBgS%^=U?;!wvaqn2J+G z+dWDt9+YQ@PULxVXh!)VSrmJ7;`yAZ58+Ic(ldRavv^Y@M)~1seq7FrJtk9b7F1Op z!Zc_&IBk+Vo>`p=pE;Md=q`otlEt6q;lNcuU%w%y!$!SuKu7s82>En=Zc3E-E+q%r z5>#f6AFwbHtrq14QOTzuHw&D2tn*z1e_QzV)BQc5a;M{v#SiowrN|i{6HVRWh_DN#(D zm)091D#|JiNg#5#1fsW$=|6UAbc8qBJtyU60iom)voP}&(}zd~23$%LEyT|OzyX2d zeJR6mu<(dm@?Jc&g&=y{Nb(L%RD;q3y45;3= zRj&Q7Y<*!%MD>PNxwZo9xy3dX8bgaOzvKOi^%eZ}cl_<}Q~mAhfrj5i{~@}3PVJgf z+Y)Pmr0PjR+up#^V8PR{+*I%cmR>4&Ld$O#o%UMSqNC_xoVyCG9q%7|@0i;8+$Yn^ z$JSa;-U*(3=SYz=vW~xZT+Xd1+zRdaRp-Yq zEF52V`+phzMf3|#)ABxO=klrLmbLn)3ZB|h+ri&5P)B3e)#25~ry)bFJ5ltqPS^cv zs;2IKz~yliDVN=Gx1MSkp}$%8x7?>_=dMzx{>)rG^e0Yj8(0hM{M@tie~Jv`etVar z8jgKm?(dY}!E{~x(NK{77dHf-asdomYcTBVfbd#PaIk~A)=3ZYwrgEBq2M!zWLA3l!vW@omj?QV&knga0(PVu8KhfiD0U;{ z9s-^&e#pn%Vk$g#HM&vaul8Eyj(bY+ghh zU_`JoVlmBIJ{%^cq*e_UKq{XUCrukp^O_Vfx3t=dDA7~;)7DLyBMqof<*@2Uk00tc6b9kuWKl>NO4qCYtf06i}KYL z-AH*5RC|Gv8=;~E;M>xJhsMtkQ!fBGcqFE_$8071wu7Kp`&>aTYtFg&N$6cnZ;{>ISI|VP{c*1y%JIiQQ@S80YB%1?FS+bN$NR}(F?fr%)ZLndh3Zh|&|R7+>? zG<3pGZRlM02YxyKi}~ee)NM(%SzPl^tK9Sh2eo^j>T3VMyZrVl7x|=D9eedJZ>!Hw z78%O!1Kebf6k4{tAAK+Sfwb1rf2Y2G(N*NA_8nJ`si9{UhQ4AO)78=_=@LqjWeRf1`7%kcXa^0htHs1q^Y4j}~g$g^m zm!hklw!-#ZO9xgxTi3gLug?CdSKW7Vwfp4akQ&&&>gfdi=&43rwlw{R#BF!CYU{q+ zNd*VKS$8)8ys}41J^aI=tIY3@s?EJ?{=Uz-K7?2H$hUW^p|x-B`>5(xL}UHyo!wV= z{r)^wzxCfd+Xx8j|4z9K1A1}W;C}j#?GSv*x-hJ*I~1Zm-A*5Bv3=TULwbiB)1Sfc z8q+q|NL|}P4+d@5IzmVvXod9aEi`am54i^e%=IpQ(8sKBG|($fJLInT>_`U~q=Ph+ zT4{6-?qgQA`UdwfD+7dQj~!F?Ax{O8LNGv()Vnb2KMYA#xN)smimAx4x`ZopEI6x- zzIvKLBI6M%`7KW5GIDzizeV?bulk*3U0L@3Jf?oDgSt{uce%X{eXuyFH{^KBeTsMh zb^3JF>4=l-3UUUFXm)T$r$o(-J_cDL3Xg1Y&6bPLC~$0n+hQgI*A9xt;?bRv!HP>} zB#k3hKH^q(I-f?>C=(cJ(@z_MMSEe`HCs|jfS>x&VDt)bDgO!qg3*&S9l1-wb?^c+ z82vI3T&}wkE!r^xpww{fvLwoxZeb^hx+s6(@~%tL^0v=?ZACXp^H6U8yUL~TQu1?G za}j)mP>YYMslR;cigS7J^J>26Cu!9HZ7*-TbOy?L+lnyPhfu8^f0YKXk7z>V_Oc}HRkOt_y_Z6sR3xVoYKNyw+TsFZVsm(@eBQnM4Y28Kr zWZ9h7S|U`|6RR>@<2`~4!C;Ir;?L?(Ic78BCv?ahvm5aV9Uzxn^C=xn##kf&?*u=o zk2B)$3e_ZTy5fH;)R4H#$p5oYi*Ya}%#qqpM#{aJX(0r_VP0p#Z2?0Y8`^>;>Uenu9=)^ZPBuKV3h@(5IX;sH6GOk3 zRU%>%O$>4+i&x5J14BQz!7;3JcA&G+xXg`1&F3uUKo8%cU+xkntPw7VdGWk~`!i`# zHyFyG6b^Fqc^hugbKqs?_4d%;XjY~dA{rygA1d7g@+74+4oQAvaYJCexJA=cRANcz zyN-^ZJVEZqQ*ktQz!7S=rA_!7kU75?!ffuj|oy*XKWfnbw&rH1z_q<9Ue>e_4+yspIl*qr?k&e%a9WZfy zeSIBoVYlAwkdlxvuyfa2#-L;*7>vA<11o88bXZR`HEfhWg15lXJa#ayD4B!=ViI$z zJcB&C)b)afLI$6_u9;yf-$-Z9rNrcni1)jO6CPIA~@mKIuba+IX&%_PHB@(3o5LhuMXZwKZR zUp|w~=gMXuc<-?Z-Prp<(xMe8_haxp1_B0X1a9!!Z?_nKH4lAlB9B7}g6+OG?Y|dj z(*XR!U316X0zcK=QZkOzZ9h?)<7@sYm7Ds4^{VyZ+ibX;yM50`XFr-%_n%R>{a9_D zSo6QCa<2m4QsZs5>5os98Mlq9&0}l+7gg>>GkbaNR@*KM$IEK-D{KDKDtEf*pjz7& z1}i-Nm6rqs)_;p_DYR@|JhRF*7FhQ!wxQ6_vKV{EQ($Xuv7th!)3{sk-C~;y&07{v zzvC~ku8VIMf=!Drz2n|kgN3bVbAlo3u338JwmXCd?Y{T+;T^%?^1ii};XC!ii$kVw z{=(I5A8{XtKbcTZomSh<+)mEi4$P{a*)M7vf7AV|Znb^)N3VV~s)h#tvU{m}t#<5= zXH4~snfDIi{U5*eaYpSNS!*4=6C7PUvPrdP)V3er7H4kv|+KR8OsV=pnyt#rqV}J84K?+0_K}4M?xC z-SD;`-36|C+l?^t-1H&OO+Srv4G9Aztal%AGdFquh=cizqJjR5##TOa*pYTKK!;r# zHH52<`WhXWn$Og@$w|EX1d{};oe_A#wsAQI9P-N$424~oraw>xJ2?p-#K1=_Jl5gJ zUC5%5gC1%vV17`8-^KLl7hdHnIC5P@+#h5Qd`J;DMo`s9md`jFe!4TaB9tt+j!FSA#Y0TnRuE$^gldKqZ;DZ@Jky4{Z23iC;5>ThI99UR6rRmWcto6& z=i`!qVd` zA)P%}W|Ga|IQ*4ZJ14ssjXjuIfFHLZ_fSd%5eWu>k$>{kz&}y>egR$=Q52mr?RgEM zB=j|*6U;orGc)j|^zfz`(;Ch8*bEvW-aKPATKxJimWui7zgRc@V%zkKebXtggk{YL5JC=+qLb;_ZU! zQxf7baBUPbiBvu*#KepYBl1L4ESn=gvSyV8B`;;VSdEnxNn^!~qM1*Fr@=o~h>dhy zk%>=&+S$lSS&-yNA}eKtl$ehQnR9YvRu7%tR5lS$^(KXL5lP5pD?a35R&O$!$dfRN z6RDAOBDXd%Mj9j%<%o_uoLkViSS+5&WMRAzi%F0R$PNGU&){_teSmjIh}hOBnRKV7h1`!&p6u}MI7R71MCJa zmOU>4m%W5bBLbw3jj|!3?1tP?#5>MFI$mHdvPUdVl&c8zO>jG-jtDbD*EqtNckR|~ z0Xi_V&x)R-=&K$<&rZDJQ#!l+(*HzBv>!T*#A7%N5 zs5!#ss-hObLiBm@Wj;Wx6SeB?G~Y1&U=~6|R$YvA?HhY}QpH7Jh98V+!S%zU1HDXA{ zvtkN(<$+HM$FZCf65^~VBr&OefZIV@FVSK&2&HEblZVuyP|Zl90L3SOz=^yh0hG0B zR)%sS54@p7ttdz_4npBtW)aWjQVXPv&F6?TGE^^2@cK#?CYCB)m`h}*#90M*CWVxs z2wfC_KCcHV=CfjEj`kTPc1eGbRI-408p>`lofeWJ2u&?OnH|(?w4lyiR&e}WTujBu zPy-Sv081(@Wb+cPDDiM-28>#&x}l)N89_)2$u2OSG)FCmK{P4H2Jx|sfU~ohnzE;4 z4*1cP1Ihpr22?O>jprbq1~VHP5g#TLFw$ByB&n^zK@qSp)P>|&P~uTp+YaIv=LPVW zM4~{*Q1yuWbQL#nqs}TZ(Ff3ycn}Zk5hh&7iP8d%6fg%KpE`#3?CR?$P@tH_okDmn zjDrJx13P>B2YPq(2fLmwfmpdLfwVAG%fzKRI})!!u#|3#C-EdEEI&|F>u%V;W@cgs zSrG8KcnbWsxrf(VffoPZOIp~+Q+PfPr?*}>10+ipC1(Tbj|$sjD6z$a?i0hDrDH<{ zC$MM=4uT|BusokVpCJHASuf-B^I5EbwW;rEChJ@vS4pZv`sohe{0uUPM@UA}SlqA2 zU_2#f@vNXE<|{+GHwCGkG6DnZVF~L2PY*GAR8LGDn>5^WUR35QQxPWx1yV>R`5h++ zXGIbfL^DVLVwn;StiCjPnUY_1L7~N-P#BXi2*Kb`4-Ua_@>5C|pa5z}pB)M>wnt#W z$im7PsmOKX=wux0CN+=YO?@UD=WIz~Hl9x@i*4l$epC_a1{~6u&|=M+D1Cw)S+pCX zBpdA8noSp@`6|#DD71#hbjC^s?W$Y>Hbp^9u{dg|LYL!5EUyD5Lvg2(?LC;Ld8nM` zp{~U#gAq;OCow8HL=VX|<3A=Z!xA|IO5~ssDJ+E)3>o${cms#=? zFmPai8-|hq32HIa<&famhBOCoj?w*DY9(z;GM~=LrAegr);OMQz-eKaq=3fe@``5F zw>J+}hQ`3A7Lck46KjHA?oi1ZD_lrOaFU{cWQav_*N|M$Y^A+6DHFLJ`lBzBl8=mc zm#(6W8xg4=I_Z_bG0^2pIBY+7fkE!3)vB!*NA9_*@3*cvL> zkFRvB_|+4<+CHTQrdK^DRr^VRae2#YuNF;QnRnN|s}om;-npQLj;}VIxKnrH(rBS$ zV0q#l$I5PX+tj6z&ph2llco9)QyBc-U%LO)UGP7DpEd2P`v#eI)?FS^8~Z-4?SJ4z zwSIzZY4+{JZQp>(4HWG}7Z6u))qmn@(UC8mS39TF=IK@6NtHYKnWwR${}r`aTJ^~) zC$I1Sk=p#@Ro|;B_bS+@s&VN^feYxbqRot*m&~9)*YJs}z0lr&x#eT7wcvu*w!*d@ zYIWt+w*&>?m4JHkl=_1?)jNOJ-}u17*1C&`t#TK=E=#1~u6;fI^R(LBw;WjJu8yfq z!>jI*JN6OPKB9Z*p0{D?xq{cP2DTTxO-pAA-a6HPs_3v(x$m%>gk30{TNk2K*ax}oa8>}5i=-pk+*nac7yIX<&tG^Ta z-(&m}9q7FVW_+*ty+AdgduzuJplb{>K4iXTvJu+R2>sXEx5N1Ny@cEQeSPr$pfzxK z8~R`$bGXI)!B7*>ANrZYLGyte~ z#a?&e@qYIDc81VB7MOLt-$LkJETQ)@ApQCf14ynPa80zaH=OQ?X7+~P3jH^lEu^Q7 zC3J`Da1(o@A0Mt~Z&ouv->f0rZq{1}-9)(EY+(S~%^-bma~)}9Z}#CMb?hxK1N5zG zGX7Sbh0u*;{H_;2}^hZ@>7&-6`tiT|CY-?Q6g>qc}!W6Xv?B4|D}xU*m3l0TXxnCgQj$dHFqVgOLE!S zrDUl0n!Lff~aWG>Q)yGdd{JTIF5f#g>1F3uyC8e2@v>3*8&{0J@n0zOVP0$ z=hP0!**7z9-h1=r&Ae}R1pGXL5^nv6`X@g^pVNk2*c$Qh5D@c7L;rVAz9WdnrpCyt)NhNk85ghud? zI;l@dM8PGZomX;(-p})AU{Fu-7*8rj*6zpm=HJD6RW~rqB8ov&Wm>@!)=NrGoltYO z0Nh(DpUXoV*h)m5mJB7qk0%(DJyM)aILv^p@n~LEiiR1?<)uO?TaqS~tgg-}IRLao8=9Yv6J=(5?(u z=W#dCo(7)BrFx$_!Z*x+e2(v_Bf|Pf(g!=_zfWy8`I^(SsyuGGYhE-t>Lb%z_mt_> zh^!DZXnnG>N?`v~t!VnxqHd^0*=jf^iB>L|Zr#ubo$n-CUeQe#Y+;!c6RgRqunXKd z*mINB4FWsmg1{(9hE8`OZswD(>I%`5IgJ#Ryjo5w#c4e`QHz_Mc`Ya9cgo6ik|-su zVW14NcFJ0=T!3wss4dAtu2i3xr41T}deRQD{?e=|WV2GSsKEp!nQq zBlJCnaXql=yP?Q}{2pInY1bG2aK%Z9H5c*)DsGGPpirdZwMZTb!HUl!1r*&}@mpj7 zHMJ}l@3mHfRx<=l3(aq(Dq*YHgaYA(y>GoeF;J9jr~OY`nHWe1#Gq}_6RQiJ*e`CR-KA8u z7R8XI)m>}GW5zI8%^xm4k>MOJEzB9oSOg}1xo*563bVzK@wAj+p zA=}gSm9=a%nr5FLfjcxR#*(g$ch8Lmnd1(OGJ-AJh2%cE8zuiUtzt4hd)67hZP9w% zmK_}sX(j)Q{;t|rWmNP6+ksTZjgJ52|DyHw|6c1pR%H^K%5$*g$7$xZQ>UG4b(&-$ zNn=?tBsCAs3H)S6hKzoMOj-#Y+cP1vr10njZBDDQBKOgh4*5W@i=yDbn39Yq5_~@Z zgU1WV(F&?zG>VZh&irA1K8{)kK;oQ}`6rxL7WnQwZ+W4T%^! zhE?rO^kc|tGS-UuSv&&@Y;oWktm+tYP~AK0@+^pw!lI&=^B|}MLa7?uyZhe!JNtI^ zf%_&T7{a@E)vU0Ra|)lA%2!Rcq{b7L2Ws1ISy5eu1xy;H(GHL@ttvALiTB}nPRr+| zbAV$2%g{Ci0Ss&;j~A=qOi?VxW2aA!W570a)ibIwWp8z_o~XyWRFv(I1*n1xaH3#W zq<-FXDuifcZr{-8@ad5w;$Zg7$zy}#G)8J+Cqvj_DVI}ph=?lWwKeGDb4^JY(#l1n zj&qW8{5nnXorSysc)4rQLueKzG-(ogPZD~?5}GYZ2&%d86A~$CD@jBu=mZ`Rh)@JF z=dCL-JvPKLot7!4XG+qu1&t^s2Z654?T52qMe>f_bTQT$v<_cUxd7*YhPXO`?MZa) z1{~S`gpa`8giJ3TvCf{!o!0?^I^}Yqq?;^UPNbP~2qJhuEhvN3mxQ8jlUH>bp;9pi zR?wg{**qNHFmb~u@z81`Khzhv?h(_Kt3DS?wD02Z-AL^H)Gt$uvzL38I|u%J^skY_%fjI`9)Vr;yv{g((e+3`LSz;9EPUsd(DA7+wA$IVaNyF( zii52yTkc7Hw107IrG4y9bnMb_g-8C#M&g=4Tm{6%Zu{b^+mZ|4y(Mg2^@VQx+BXoo z^rH_gnJ?XmzI1VT&4b#ymtOlozSFwrcI%!?Gaok1kKFYI-%h=m`b>x}dcf{?PcLqI z=j>|3Qlh_M7e{{Wzr6X@rUB?*Za!Y|yS@Bc2!)&1BD~Z@#YXgj*PnHlQkuC{WN z?rfv|spxPw`Xs~*cX6MDIZC%2iNV0NJ_ZC`OZtYo*=uk?V)l9~1N8MaCyZXlPD*#P zl8VL7iHA_t(4s@Jzd1>4Fn4;)U0dl*2B@OKpb Q6217yMc-l7XQ3tHzu?3+9RL6T literal 0 HcmV?d00001 diff --git a/mcp/figshare_mcp/tools/account.py b/mcp/figshare_mcp/tools/account.py new file mode 100644 index 00000000..26b6db1b --- /dev/null +++ b/mcp/figshare_mcp/tools/account.py @@ -0,0 +1,69 @@ +""" +MCP tool for Figshare account information. + +Tools: + get_account_info — retrieve profile, licenses, categories, and institution embargo options +""" + +import json + +from figshare_mcp.client import FigshareClient + + +async def get_account_info( + include_profile: bool = True, + include_licenses: bool = True, + include_categories: bool = True, + include_embargo_options: bool = False, +) -> str: + """Retrieve Figshare account metadata: user profile, available licenses, categories, and embargo options. + + Use this tool to look up valid license IDs or category IDs before creating/updating articles. + + Args: + include_profile: Include the authenticated user's profile details (requires token). + include_licenses: Include the list of available licenses and their IDs. + include_categories: Include the Figshare taxonomy of subject categories. + include_embargo_options: Include the institution-level embargo configuration (requires token). + + Returns: + JSON string with the requested account information sections. + """ + client = FigshareClient() + result = {} + errors = {} + + if include_profile: + if not client.has_token: + errors["profile"] = "FIGSHARE_TOKEN is required to fetch profile" + else: + try: + result["profile"] = await client.get("/account") + except RuntimeError as exc: + errors["profile"] = str(exc) + + if include_licenses: + try: + result["licenses"] = await client.get("/licenses") + except RuntimeError as exc: + errors["licenses"] = str(exc) + + if include_categories: + try: + result["categories"] = await client.get("/categories") + except RuntimeError as exc: + errors["categories"] = str(exc) + + if include_embargo_options: + if not client.has_token: + errors["embargo_options"] = "FIGSHARE_TOKEN is required to fetch embargo options" + else: + try: + result["embargo_options"] = await client.get("/account/institution/embargo_options") + except RuntimeError as exc: + errors["embargo_options"] = str(exc) + + if errors: + result["errors"] = errors + + return json.dumps(result, default=str) diff --git a/mcp/figshare_mcp/tools/articles.py b/mcp/figshare_mcp/tools/articles.py new file mode 100644 index 00000000..a430d58c --- /dev/null +++ b/mcp/figshare_mcp/tools/articles.py @@ -0,0 +1,264 @@ +""" +MCP tools for Figshare articles. + +Tools: + search_articles — search public or private articles + get_article — retrieve article details, files, and versions + manage_article — create or update a draft article +""" + +import json +from typing import Any + +from figshare_mcp.client import FigshareClient, clamp_page_size + + +def _format_article(article: dict) -> dict: + """Return a trimmed article dict with only the most useful fields.""" + return { + "id": article.get("id"), + "title": article.get("title"), + "doi": article.get("doi"), + "url": article.get("url_public_html") or article.get("url"), + "published_date": article.get("published_date"), + "modified_date": article.get("modified_date"), + "status": article.get("status"), + "defined_type_name": article.get("defined_type_name"), + "authors": [a.get("full_name") for a in article.get("authors", [])], + "tags": article.get("tags", []), + "categories": [c.get("title") for c in article.get("categories", [])], + "files_count": len(article.get("files", [])), + } + + +async def search_articles( + query: str = "", + private: bool = False, + page: int = 1, + page_size: int = 10, + order: str = "published_date", + order_direction: str = "desc", + institution: int | None = None, + published_since: str | None = None, + modified_since: str | None = None, + group: int | None = None, + item_type: int | None = None, +) -> str: + """Search Figshare articles. + + For public articles use private=False (no token needed). + For private/draft articles owned by the authenticated user use private=True (token required). + + Args: + query: Free-text search string. + private: If True, search within the authenticated user's own articles (requires token). + page: Page number (starts at 1). + page_size: Results per page (1–50, default 10). + order: Sort field — e.g. published_date, modified_date, views, citations. + order_direction: "asc" or "desc". + institution: Filter by institution ID. + published_since: ISO 8601 date string (e.g. "2023-01-01"). + modified_since: ISO 8601 date string. + group: Filter by group ID. + item_type: Figshare item type ID (e.g. 1=figure, 3=dataset, 9=software). + + Returns: + JSON string with matching articles and pagination metadata. + """ + client = FigshareClient() + page_size = clamp_page_size(page_size) + + if private: + if not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to search private articles"}) + # Private search uses POST with a body. + body: dict[str, Any] = { + "page": page, + "page_size": page_size, + "order_direction": order_direction, + } + if query: + body["search_for"] = query + if institution is not None: + body["institution"] = institution + if published_since: + body["published_since"] = published_since + if modified_since: + body["modified_since"] = modified_since + if group is not None: + body["group"] = group + try: + results = await client.post("/account/articles/search", json=body) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + else: + # Public search uses POST /articles/search. + body = { + "page": page, + "page_size": page_size, + "order_direction": order_direction, + } + if query: + body["search_for"] = query + if institution is not None: + body["institution"] = institution + if published_since: + body["published_since"] = published_since + if modified_since: + body["modified_since"] = modified_since + if group is not None: + body["group"] = group + if item_type is not None: + body["item_type"] = item_type + try: + results = await client.post("/articles/search", json=body) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + articles = [_format_article(a) for a in (results if isinstance(results, list) else [])] + return json.dumps( + { + "articles": articles, + "count": len(articles), + "page": page, + "page_size": page_size, + "has_more": len(articles) == page_size, + "note": "Use page+1 to fetch the next page if has_more is true. Hard cap: 1000 total results.", + }, + default=str, + ) + + +async def get_article( + article_id: int, + include_files: bool = True, + include_versions: bool = True, + private: bool = False, +) -> str: + """Get full details for a single Figshare article. + + Args: + article_id: Numeric Figshare article ID. + include_files: Also fetch the list of files attached to this article. + include_versions: Also fetch the list of published versions. + private: If True, fetch from the authenticated user's private articles (requires token). + + Returns: + JSON string with article details, optionally including files and versions. + """ + client = FigshareClient() + + if private and not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to fetch private article details"}) + + base_path = f"/account/articles/{article_id}" if private else f"/articles/{article_id}" + + try: + article = await client.get(base_path) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + result: dict[str, Any] = {"article": article} + + if include_files: + try: + files = await client.get(f"/articles/{article_id}/files") + result["files"] = files + except RuntimeError as exc: + result["files_error"] = str(exc) + + if include_versions: + try: + versions = await client.get(f"/articles/{article_id}/versions") + result["versions"] = versions + except RuntimeError as exc: + result["versions_error"] = str(exc) + + return json.dumps(result, default=str) + + +async def manage_article( + action: str, + article_id: int | None = None, + title: str | None = None, + description: str | None = None, + tags: list[str] | None = None, + categories: list[int] | None = None, + authors: list[dict] | None = None, + license: int | None = None, + defined_type: str | None = None, + doi: str | None = None, + funding: str | None = None, + group_id: int | None = None, +) -> str: + """Create or update a draft Figshare article. Requires authentication token. + + This tool never publishes — it only creates/edits drafts. + To publish, go to the Figshare web interface. + + Args: + action: "create" to create a new draft, "update" to edit an existing one. + article_id: Required when action is "update". The article to update. + title: Article title. Required when action is "create". + description: Article description / abstract (HTML or plain text). + tags: List of keyword tags. + categories: List of category IDs. + authors: List of author objects. Each can be {"name": "..."} or {"id": 123}. + license: License ID (use get_account_info to list available licenses). + defined_type: Item type string — e.g. "dataset", "figure", "software", "paper". + doi: Custom DOI (leave blank to let Figshare assign one). + funding: Funding acknowledgement string. + group_id: Group ID to associate the article with. + + Returns: + JSON string with the created/updated article details or an error message. + """ + client = FigshareClient() + + if not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to create or update articles"}) + + if action not in ("create", "update"): + return json.dumps({"error": f"Invalid action '{action}'. Use 'create' or 'update'."}) + + if action == "create" and not title: + return json.dumps({"error": "title is required when action is 'create'"}) + + if action == "update" and article_id is None: + return json.dumps({"error": "article_id is required when action is 'update'"}) + + # Build the request body with only the provided (non-None) fields. + body: dict[str, Any] = {} + if title is not None: + body["title"] = title + if description is not None: + body["description"] = description + if tags is not None: + body["tags"] = tags + if categories is not None: + body["categories"] = categories + if authors is not None: + body["authors"] = authors + if license is not None: + body["license"] = license + if defined_type is not None: + body["defined_type"] = defined_type + if doi is not None: + body["doi"] = doi + if funding is not None: + body["funding"] = funding + if group_id is not None: + body["group_id"] = group_id + + try: + if action == "create": + result = await client.post("/account/articles", json=body) + return json.dumps({"success": True, "action": "created", "article": result}, default=str) + else: + # PUT replaces all fields; PATCH updates only provided fields — we use PUT here. + await client.put(f"/account/articles/{article_id}", json=body) + # PUT returns 205 with no body; fetch the updated article to confirm. + updated = await client.get(f"/account/articles/{article_id}") + return json.dumps({"success": True, "action": "updated", "article": updated}, default=str) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) diff --git a/mcp/figshare_mcp/tools/collections.py b/mcp/figshare_mcp/tools/collections.py new file mode 100644 index 00000000..ff3f7ee8 --- /dev/null +++ b/mcp/figshare_mcp/tools/collections.py @@ -0,0 +1,226 @@ +""" +MCP tools for Figshare collections. + +Tools: + search_collections — search public or private collections + get_collection — retrieve collection details and its articles + manage_collection — create or update a draft collection +""" + +import json +from typing import Any + +from figshare_mcp.client import FigshareClient, clamp_page_size + + +async def search_collections( + query: str = "", + private: bool = False, + page: int = 1, + page_size: int = 10, + order: str = "published_date", + order_direction: str = "desc", + institution: int | None = None, + published_since: str | None = None, + modified_since: str | None = None, + group: int | None = None, +) -> str: + """Search Figshare collections. + + Args: + query: Free-text search string. + private: If True, search the authenticated user's own collections (requires token). + page: Page number (starts at 1). + page_size: Results per page (1–50, default 10). + order: Sort field — e.g. published_date, modified_date, views. + order_direction: "asc" or "desc". + institution: Filter by institution ID. + published_since: ISO 8601 date string (e.g. "2023-01-01"). + modified_since: ISO 8601 date string. + group: Filter by group ID. + + Returns: + JSON string with matching collections and pagination metadata. + """ + client = FigshareClient() + page_size = clamp_page_size(page_size) + + body: dict[str, Any] = { + "page": page, + "page_size": page_size, + "order_direction": order_direction, + } + if query: + body["search_for"] = query + if institution is not None: + body["institution"] = institution + if published_since: + body["published_since"] = published_since + if modified_since: + body["modified_since"] = modified_since + if group is not None: + body["group"] = group + + if private: + if not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to search private collections"}) + # Private collections: use GET with query params (no search POST for account collections). + params = { + "page": page, + "page_size": page_size, + "order": order, + "order_direction": order_direction, + } + try: + results = await client.get("/account/collections", params=params) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + else: + try: + results = await client.post("/collections/search", json=body) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + collections = results if isinstance(results, list) else [] + return json.dumps( + { + "collections": collections, + "count": len(collections), + "page": page, + "page_size": page_size, + "has_more": len(collections) == page_size, + "note": "Use page+1 to fetch the next page if has_more is true.", + }, + default=str, + ) + + +async def get_collection( + collection_id: int, + include_articles: bool = True, + articles_page: int = 1, + articles_page_size: int = 10, + private: bool = False, +) -> str: + """Get full details for a single Figshare collection, optionally including its articles. + + Args: + collection_id: Numeric Figshare collection ID. + include_articles: Also fetch the first page of articles in this collection. + articles_page: Page number for articles listing (starts at 1). + articles_page_size: Articles per page (1–50, default 10). + private: If True, fetch from the authenticated user's private collections (requires token). + + Returns: + JSON string with collection details and optionally its articles. + """ + client = FigshareClient() + + if private and not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to fetch private collection details"}) + + base_path = ( + f"/account/collections/{collection_id}" if private else f"/collections/{collection_id}" + ) + + try: + collection = await client.get(base_path) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + result: dict[str, Any] = {"collection": collection} + + if include_articles: + articles_page_size = clamp_page_size(articles_page_size) + try: + articles = await client.get( + f"/collections/{collection_id}/articles", + params={"page": articles_page, "page_size": articles_page_size}, + ) + result["articles"] = articles + result["articles_page"] = articles_page + result["articles_has_more"] = len(articles) == articles_page_size + except RuntimeError as exc: + result["articles_error"] = str(exc) + + return json.dumps(result, default=str) + + +async def manage_collection( + action: str, + collection_id: int | None = None, + title: str | None = None, + description: str | None = None, + articles: list[int] | None = None, + tags: list[str] | None = None, + categories: list[int] | None = None, + authors: list[dict] | None = None, + doi: str | None = None, + group_id: int | None = None, + funding: str | None = None, +) -> str: + """Create or update a Figshare collection. Requires authentication token. + + This tool never publishes — it only creates/edits drafts. + + Args: + action: "create" to create a new collection, "update" to edit an existing one. + collection_id: Required when action is "update". + title: Collection title. Required when action is "create". + description: Collection description (HTML or plain text). + articles: List of article IDs to include in the collection. + tags: List of keyword tags. + categories: List of category IDs. + authors: List of author objects. Each can be {"name": "..."} or {"id": 123}. + doi: Custom DOI. + group_id: Group ID to associate the collection with. + funding: Funding acknowledgement string. + + Returns: + JSON string with the created/updated collection details or an error message. + """ + client = FigshareClient() + + if not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to create or update collections"}) + + if action not in ("create", "update"): + return json.dumps({"error": f"Invalid action '{action}'. Use 'create' or 'update'."}) + + if action == "create" and not title: + return json.dumps({"error": "title is required when action is 'create'"}) + + if action == "update" and collection_id is None: + return json.dumps({"error": "collection_id is required when action is 'update'"}) + + # Build body with only non-None fields. + body: dict[str, Any] = {} + if title is not None: + body["title"] = title + if description is not None: + body["description"] = description + if articles is not None: + body["articles"] = articles + if tags is not None: + body["tags"] = tags + if categories is not None: + body["categories"] = categories + if authors is not None: + body["authors"] = authors + if doi is not None: + body["doi"] = doi + if group_id is not None: + body["group_id"] = group_id + if funding is not None: + body["funding"] = funding + + try: + if action == "create": + result = await client.post("/account/collections", json=body) + return json.dumps({"success": True, "action": "created", "collection": result}, default=str) + else: + await client.put(f"/account/collections/{collection_id}", json=body) + updated = await client.get(f"/account/collections/{collection_id}") + return json.dumps({"success": True, "action": "updated", "collection": updated}, default=str) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) diff --git a/mcp/figshare_mcp/tools/embargo.py b/mcp/figshare_mcp/tools/embargo.py new file mode 100644 index 00000000..76e578cc --- /dev/null +++ b/mcp/figshare_mcp/tools/embargo.py @@ -0,0 +1,124 @@ +""" +MCP tool for Figshare article embargo management. + +Tools: + manage_embargo — get, set, or remove embargo on an article +""" + +import json +from typing import Any + +from figshare_mcp.client import FigshareClient + + +async def manage_embargo( + action: str, + article_id: int | None = None, + is_embargoed: bool | None = None, + embargo_date: str | None = None, + embargo_type: str | None = None, + embargo_title: str | None = None, + embargo_reason: str | None = None, + include_institution_options: bool = False, +) -> str: + """Get, set, or remove embargo on a Figshare article. Requires authentication token. + + Embargo controls public access to an article's files until a specified date. + + Actions: + "get" — retrieve the current embargo status for an article + "set" — apply or update an embargo on an article + "remove" — lift (delete) the embargo, making the article publicly accessible immediately + "options" — list available embargo types for your institution (no article_id needed) + + Args: + action: One of "get", "set", "remove", "options". + article_id: The article to act on. Required for get/set/remove. + is_embargoed: Whether to enable the embargo. Required for "set". + embargo_date: Embargo expiry date in ISO 8601 format (e.g. "2025-12-31"). Required for "set". + embargo_type: Embargo type string (e.g. "file", "article"). Required for "set". + Use action="options" to see valid types for your institution. + embargo_title: Optional human-readable title for the embargo notice. + embargo_reason: Optional explanation shown to users who try to access embargoed content. + include_institution_options: For action="get", also fetch institution-level embargo options. + + Returns: + JSON string with the embargo details or a confirmation of the action taken. + """ + client = FigshareClient() + + if not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required for all embargo operations"}) + + if action not in ("get", "set", "remove", "options"): + return json.dumps({"error": f"Invalid action '{action}'. Use 'get', 'set', 'remove', or 'options'."}) + + # Fetch institution-level embargo options (no article needed). + if action == "options": + try: + options = await client.get("/account/institution/embargo_options") + return json.dumps({"embargo_options": options}, default=str) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + # All other actions require an article_id. + if article_id is None: + return json.dumps({"error": f"article_id is required for action '{action}'"}) + + if action == "get": + try: + embargo = await client.get(f"/account/articles/{article_id}/embargo") + result: dict[str, Any] = {"article_id": article_id, "embargo": embargo} + if include_institution_options: + try: + result["institution_options"] = await client.get( + "/account/institution/embargo_options" + ) + except RuntimeError as exc: + result["institution_options_error"] = str(exc) + return json.dumps(result, default=str) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + if action == "set": + if is_embargoed is None: + return json.dumps({"error": "is_embargoed is required for action 'set'"}) + if not embargo_date: + return json.dumps({"error": "embargo_date is required for action 'set' (ISO 8601, e.g. '2025-12-31')"}) + if not embargo_type: + return json.dumps({"error": "embargo_type is required for action 'set'. Use action='options' to see valid types."}) + + body: dict[str, Any] = { + "is_embargoed": is_embargoed, + "embargo_date": embargo_date, + "embargo_type": embargo_type, + } + if embargo_title is not None: + body["embargo_title"] = embargo_title + if embargo_reason is not None: + body["embargo_reason"] = embargo_reason + + try: + await client.put(f"/account/articles/{article_id}/embargo", json=body) + # Fetch the updated embargo to confirm. + updated = await client.get(f"/account/articles/{article_id}/embargo") + return json.dumps( + {"success": True, "action": "embargo_set", "article_id": article_id, "embargo": updated}, + default=str, + ) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + # action == "remove" + try: + await client.delete(f"/account/articles/{article_id}/embargo") + return json.dumps( + { + "success": True, + "action": "embargo_removed", + "article_id": article_id, + "note": "The embargo has been lifted. The article is now publicly accessible.", + } + ) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) diff --git a/mcp/figshare_mcp/tools/projects.py b/mcp/figshare_mcp/tools/projects.py new file mode 100644 index 00000000..b7218781 --- /dev/null +++ b/mcp/figshare_mcp/tools/projects.py @@ -0,0 +1,103 @@ +""" +MCP tool for Figshare projects. + +Tools: + get_projects — list or retrieve a specific project (public or private) +""" + +import json +from typing import Any + +from figshare_mcp.client import FigshareClient, clamp_page_size + + +async def get_projects( + project_id: int | None = None, + private: bool = False, + page: int = 1, + page_size: int = 10, + order: str = "published_date", + order_direction: str = "desc", + institution: int | None = None, + group: int | None = None, + storage: str | None = None, + roles: str | None = None, +) -> str: + """List Figshare projects or get details of a specific project. + + Args: + project_id: If provided, returns details for that specific project. + If omitted, returns a paginated list of projects. + private: If True, lists/fetches from the authenticated user's own projects (requires token). + page: Page number (starts at 1). Used only when project_id is not provided. + page_size: Results per page (1–50, default 10). + order: Sort field — e.g. published_date, modified_date. + order_direction: "asc" or "desc". + institution: Filter by institution ID (public listing only). + group: Filter by group ID (public listing only). + storage: Filter by storage type ("individual" or "group") — private only. + roles: Filter by role ("viewer", "collaborator", "owner") — private only. + + Returns: + JSON string with project(s) details and pagination metadata. + """ + client = FigshareClient() + + if private and not client.has_token: + return json.dumps({"error": "FIGSHARE_TOKEN is required to access private projects"}) + + # Fetch a single project by ID. + if project_id is not None: + path = f"/account/projects/{project_id}" if private else f"/projects/{project_id}" + try: + project = await client.get(path) + return json.dumps({"project": project}, default=str) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + # List projects. + page_size = clamp_page_size(page_size) + + if private: + params: dict[str, Any] = { + "page": page, + "page_size": page_size, + "order": order, + "order_direction": order_direction, + } + if storage: + params["storage"] = storage + if roles: + params["roles"] = roles + try: + results = await client.get("/account/projects", params=params) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + else: + params = { + "page": page, + "page_size": page_size, + "order": order, + "order_direction": order_direction, + } + if institution is not None: + params["institution"] = institution + if group is not None: + params["group"] = group + try: + results = await client.get("/projects", params=params) + except RuntimeError as exc: + return json.dumps({"error": str(exc)}) + + projects = results if isinstance(results, list) else [] + return json.dumps( + { + "projects": projects, + "count": len(projects), + "page": page, + "page_size": page_size, + "has_more": len(projects) == page_size, + "note": "Use page+1 to fetch the next page if has_more is true.", + }, + default=str, + ) diff --git a/mcp/pyproject.toml b/mcp/pyproject.toml new file mode 100644 index 00000000..5f842334 --- /dev/null +++ b/mcp/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "figshare-mcp" +version = "0.1.0" +description = "MCP server for the Figshare API" +requires-python = ">=3.11" +dependencies = [ + "mcp[cli]>=1.0.0", + "httpx>=0.27.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", +] + +[project.scripts] +figshare-mcp = "figshare_mcp.server:main" + +[tool.hatch.build.targets.wheel] +packages = ["figshare_mcp"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/mcp/tests/__init__.py b/mcp/tests/__init__.py new file mode 100644 index 00000000..8e08d8ea --- /dev/null +++ b/mcp/tests/__init__.py @@ -0,0 +1 @@ +"""Integration tests for figshare-mcp — require a running local Figshare instance.""" diff --git a/mcp/tests/__pycache__/__init__.cpython-314.pyc b/mcp/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f9e0a4d95a625997011fc834ae41da1e0854783 GIT binary patch literal 293 zcmYjM%}N6?5KgRUDd+=u8t_!gHaAb=QSc&Ika`OtJ6!|2lXa35y!9b`314Xi5B3Fw zzJS@KEzV(nznSlw+2zGVWV^Zgb}u<@`{H17pX_?bMl3`W3wbT3C+R6S9w=hPCOS_L zF~k5X&#-b;cr}bgV_W#yd;sHHa*SYr6Op3|YHv*q_j@2X3ek|o`Qub}=USKEYW*p8 zJ$a|qb3hKtdM2!0Qb_BdjwHjWqQtdgZ2iAM%3DRglp3TsXjEB{8gweH9l5CWynXM+ bw~a4Tjdy&O`TCE;IJ@7Gl=4d~`7{@QDo>FFJBBvrrW}v&q*!b6z5qfm$S%Mp+`v-6MxmCf!`!P z$EyzYoaF{#c$t6}f@$dco?n~GVf@MEOV<`ISo8MhpIw<>#87^3Z{ilAEbyHMc3hV* zhIw$CC<`1cO@s9y1l3>{uM^w|TsZ9cj61$d`qf^%aBbecZhb1%l2WaN>zudd%4I21 z4;Y_2GjnF9y!rtcANu%?$Lp`#b#85Kt!HYY^W2+M;g)hBc&RKsQ^v*B+@c%9Cb1Gk zNg-tHEW)6TZlFs4ZVacTS^BK5CKGQylj@zZ41jT9q8eUii7O`@dbP}>+m8_O2 zX$5^7AxIUgfg1|CNw$`It`|255K67}XjB1XC(I@^4Hxf$%8oe*-I3-H;G(tzv%@|#TDu!C`Zz0_E zSLJF4rJOKamP#fW^?Hy?C2|vxGA6qfxG{U}w%1Z2RgB@TO7I!>Rl{*`-IItMCG^H-mz(C9QYnfIwF z?l8eR2B{G4A7F0KB9aD4*a{#86?TrxyKbN*{mfl}8P<-Vt6E*Nw41P_N*S{41N|={ z6QwEGN`=-G<5UC%B*~`3<@1iI=jCnN@xhDFyY#%gj^ylEv{j>MZcJ{s3nY#3NaG_%? zs8;D%I3J(cncck(4z2TadTF7uX|IFPI~jal(?DL z_wNCXlE6%Q@UBM1A4MTWF?2n@(RajDa8J1|TX*7YO^?CiBvdBoEqph!ssE7uKHE7o z^>}pp@!-jhaZ*Yah9lFqAvxN1lvxhF28Wy+<T7(VqF`Wh};XAej&2~!D zwC8$OGhXfo@83n=WdA@zFVZQ^)?VZdh@kCZq-VYz|9bp8?}>*0)&=o*Qb<3#Er;8g PSGS9D_XX@0r91ush&V+e literal 0 HcmV?d00001 diff --git a/mcp/tests/__pycache__/test_account.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_account.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0737c58276f42f4def82022f1b739ed494aee0e7 GIT binary patch literal 14186 zcmeHOYiu0Xb-uID*(JHlheb*hZ9dkdxRlJt@+Db`Bgz&<#jL`P&vQPiOVsCN6K z=iEDYW_GzUDb`98K#S$M=f3aUdw0%v9y}VU3lsQgt^b_a4iS>ZjQIF1=E1jtd50*( zAa4*wIwu((mU^drOP+Qz?enS*)xXGRvBlfqd&lk^Tt%7KfW!=4czuTlK2UhK{|J&@}pNspLY2`It2 zT2Vg}5uB5D5^;vOzJ+HiR#oq`6da5%iSfnL%(qnZtG+I~-|t9DaKLlSetJR*K1EhS zN#B4}R_dQ?67#yA`WaE?q_8T#Sd~bxNQ(EQL}N)ffn%=wTnNm;dif_An6q!cxYfn0 z;WRNcwBX0Z;jW-;^p;fnrMcXVfe*x6d8&b3!n`y~Tn zKDZ%~C-7X(o?5?x$9%3G{o%PHN@&;T3X10%@O|d@XranIf>RRrNX%S-H*WD}GIabK z@O%FY@!ShPuQ<=@zxU>37=G_97yrohd#_QVPvC^?--B4USm_>b-j4ooLea;%dG))V zrYzQ`H0;_oB-&P-e2vnmG$m^)-Yd0_@m{GFBa4*3R|Fm)6GUvl1_op%+rjfpEqT@+FQ6b zWC9sB160$Ovc@Nol&)*k=;-b(TpNw+SULL1XnA4y53Jgb&$D)ealIEy5^?<)D;SRJ z@$gr%@1PmZ=$V{uq;hEu3lgR;o6!x^o7HldA3-v{?4I@npZ3IU$@FA$x25Gq9DFFQ zgKW2lmrUuUxU@WgRU>hoFA12POM;bjyu|vA!?Gf#?<;yfXZo}G)Vxmd)nSHD6|NzW zP*c=wP}8YwR;5~jrt^!16xGa-s;4e%SYp;fOZC!HCTqYmnSpE`8tdjkRlky7%FeT1 z>OwYuDV0@osYT6wB!7d`8-2|dL1mPr^EFz z(*v)=h0zDnd79I*nWX_O_m)0zIkTW&fz5k?ZP5$!+FJv#fq7TO67+guKA&D%)N%&f zh=Ij)VSoWTMg?l?FI+R*+3|>LtERG7Q`dBCpy_(%65Ra6P8im|OCLmL`gh<0{5xQ> zpEVr#lh$vw{;aWUtD}3XvwJJn`>UX@De{o`>LL%kfyVHSz()~M-&&GmHMg55K!yYCZP-`L*aUWHzJ2n?POP zHEdxQtU9?3c^Ep{HJpLM@6X$1&|2iM^^URE<>9TSLpS@@o4QMK-@AILse3Kj1MIB^ zMpmMIo3iy?llw~19y@C@@9H+YievEH2DEPK=KOtEPvCDg*d5#q^w@5I1Nc}!28VUM zYr0GNGikbA{_ey{VBhaN-3!SF;fbS=d^q-UJtY4aJbeg~>KJ?w@a;Y@IHK?;i@~D| zgV#UB;KZ%3JjLK}>zPKl1H(VX;B0G11rqx#bh!l!Yf01p$*@+#0d`^F#g+`7Zs)f?E}&aIZ^>|$V`1_f%{ zdTW149z}uL|3lwebQCh1(a}wyuJ0PQunSh5+=e^~9qk&f27{>``W*XXOX%OhHkmctOH}1;b@$@>jJpd6YnLIO$<_rdNbEXW5x-ZF8ooJwQ2- z^nu>ld@pLMG*i{qQN_;WjE}#>ijUilC7#BzGdcaL zugkW7!J9Z#VQ2EAB`4{x&<6u^2gLNQr?%hhqK;YDY}<)=de8^`pa+g9q?k|wZc9_~ zYBk$xL00MtAthYA)k!XX+qOFWO|quq>{gh~zXnRSXyw$rN0~L63Gd8{w8wci22*}+8kWy^?PG~JQ$=J!2=;S6eW8XEXjJ9)r z-NoO>PM}9)GschG{GE`^-c8tSDdr5y3vCMhEzJ~B&-Kx%I_Ynvsjz$pgnFIy{>YgY zNN$t!@sKI2%oe1oW>{4h^Ycqt4ftABEiR?9XeEMNR8=o$sBUC4IW3o0RXPl#(s3ly zK+FTG3MMoolU7rPK{J<@;G#gc231XG;l|Bp(neYXvy}=qvpQEHbVHzUid`>?UL;2F zC9k7h`Ma`x!H|FKVRMcDtn?8<`T(kV>ojuWBXGw~kJ;cJJ!b7YukOT> zaRsbE8AO-!WvaUkew6*X&z`jE-ojM(gHKiW|BLG0PYMA5E5qQzgbP-@T0A1)*aGNA z;2iT6zcy0^Dj~!vga=6RM*wraXTzL($yzqfKNUPlj~EFYZU3<+2g#vi5P)jy#UU5i z3GC_|aDZy)x5gn_6{r?}F>^@#+MP5R5pblPB!gm47yf5&J33njJ9>Clo~?E5>FzE> z2k1q9bN3VgtLn>|Z+AgLFz#3(MHCh2!>yv@1 zx1ip+1+E!wN1bzDK(z|IA&Fn)04W2>4axzfIoVJ#XX9fy%o@cyL~1$AJOGZ-X@+1~ z0OdO10W2570GR*IGVXY%2?NY+7bdxJhrR@BuYkl3Af}&#+E0QAXTga?@l8Z0keo*H zG7yG-#~5WWIWf2_WAg+6T`WzM1Lg$!*06Zg4VXU(3t0eOzBk+WblqH+svJDxE_-Y#%G1gM)k?S=qnBf5_46IIhQxDA0+p`hjF z&4}=HU4l@Yd?DVnSA!Lu!eN-B&BD?adbV<-Wp(3axfAppp2wg)0{2@b%FuYC42>s7 z+}J!ZrlYN{44QLi7YFDqI{-RI=jQk>poTl6+~AydM1!3Emmu>holuX&-Qa{eRFb=H z0_5%5l&$ZY+;!s`&#gPGUA=+r?c8cv&MpQ9YJMs!H{LGEie-~ifV~CtDL?M}Q6D6C zXMxq{>xD{*C0w!RxU0?(~Gth*ZRcUI$7oXO5rF!$NqFIr7uuv>>J zTEJX)dh9Y7OysZvoK)BqM!7p{v-haY!n!%XcM4-!w!C9-aNv+Sf)1(YnM3Lb+VUhQ z`TN*J4K0wf>s)m<(4lYUVPR);^{laAcL++&UV zwmJkA`8QPZ$`z6q9C=DODVJ-1#c$0Z@~-$KZOs|14nDD`3*Xt>j?SVTr)f5jlw4Ma znqAS1?RnU&C!K=J2*NEsK;9lFX{wj`~#CG&rDv3`zWrQB9Nx&)-zi~psTicDMc6Z zYQDf;=I705MG4d8dFTcU*gzj6{e2|mYCYHAQHN#&9v~m`siu$@0 zqsn#lXKY>FDn{-5apONO;o8zjo=1W&O3Qi0{Z%a1qZg}CDdFKDs6=yRxDPqhi0K!R zyb8pv@eYlZ<&JaTlq@lhl0l058MJ^qO)RD&jHG1+~?C z>{lT_hGhCNBr}L1nZIl#p$VxJ?I_8o-UE#oRK8Qd-ns(hPR~mJDaiBh22?~lIKS@V z-3{IY9ocHWisRY23g$k}fSM*YhQV%Ksb~O`iE5~Uv0S-|rd6{r=5_--<;Oan`1RP? zA?dr)*?#%Yj{x_R$V3k$?~k62L-IlU>3T@$d1wgUCi*2v=?s$JMWP_dBFQ7UisUsU z7m&OT#O)yZJuF0m_eIAbCGH^dh5WBc+s!_I&%@fVe*!#2Na;n*HX^D8`Zyx^I;Z5oAigfs)r%l40K>EN-JZ;;o HfX(#3`Lk2z literal 0 HcmV?d00001 diff --git a/mcp/tests/__pycache__/test_articles.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_articles.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..035fff42b7f8d6568102784d53ec4754bf902e63 GIT binary patch literal 27912 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 literal 0 HcmV?d00001 diff --git a/mcp/tests/__pycache__/test_collections.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_collections.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b92d1230a36bd8407b59191d65f354160d371a6a GIT binary patch literal 17804 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 literal 0 HcmV?d00001 diff --git a/mcp/tests/__pycache__/test_projects.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_projects.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73057fdc7abf22be520774172f8febfcbc1cb1db GIT binary patch literal 9489 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 literal 0 HcmV?d00001 diff --git a/mcp/tests/conftest.py b/mcp/tests/conftest.py new file mode 100644 index 00000000..fb8f1cc3 --- /dev/null +++ b/mcp/tests/conftest.py @@ -0,0 +1,37 @@ +""" +Pytest configuration for figshare-mcp integration tests. + +Required environment variables before running: + FIGSHARE_TOKEN — personal access token for a test account on the local instance + FIGSHARE_BASE_URL — e.g. http://localhost:8080/v2 + +Run with: + FIGSHARE_TOKEN=xxx FIGSHARE_BASE_URL=http://localhost:8080/v2 pytest mcp/tests/ -v +""" + +import os + +import pytest + + +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line("markers", "requires_token: test requires FIGSHARE_TOKEN to be set") + config.addinivalue_line("markers", "write: test creates or modifies data on the instance") + + +def pytest_runtest_setup(item): + """Skip token-required tests if no token is configured.""" + if item.get_closest_marker("requires_token"): + if not os.getenv("FIGSHARE_TOKEN"): + pytest.skip("FIGSHARE_TOKEN not set — skipping authenticated test") + + +@pytest.fixture(scope="session") +def base_url() -> str: + return os.getenv("FIGSHARE_BASE_URL", "https://api.figshare.com/v2") + + +@pytest.fixture(scope="session") +def has_token() -> bool: + return bool(os.getenv("FIGSHARE_TOKEN")) diff --git a/mcp/tests/test_account.py b/mcp/tests/test_account.py new file mode 100644 index 00000000..a410ee25 --- /dev/null +++ b/mcp/tests/test_account.py @@ -0,0 +1,77 @@ +"""Integration tests for account info tools.""" + +import json + +import pytest + +from figshare_mcp.tools.account import get_account_info + + +class TestGetAccountInfo: + async def test_licenses_always_accessible(self): + result = json.loads( + await get_account_info( + include_profile=False, + include_licenses=True, + include_categories=False, + ) + ) + assert "licenses" in result + assert isinstance(result["licenses"], list) + assert len(result["licenses"]) > 0 + + async def test_categories_always_accessible(self): + result = json.loads( + await get_account_info( + include_profile=False, + include_licenses=False, + include_categories=True, + ) + ) + assert "categories" in result + assert isinstance(result["categories"], list) + + async def test_profile_without_token_returns_error(self, monkeypatch): + monkeypatch.delenv("FIGSHARE_TOKEN", raising=False) + result = json.loads( + await get_account_info( + include_profile=True, + include_licenses=False, + include_categories=False, + ) + ) + # Profile fetch should fail, others should succeed or be absent. + assert "errors" in result + assert "FIGSHARE_TOKEN" in result["errors"].get("profile", "") + + +@pytest.mark.requires_token +class TestGetAccountInfoAuthenticated: + async def test_profile_with_token(self): + result = json.loads( + await get_account_info( + include_profile=True, + include_licenses=False, + include_categories=False, + ) + ) + assert "profile" in result + assert "errors" not in result or "profile" not in result.get("errors", {}) + + async def test_all_sections(self): + result = json.loads(await get_account_info()) + assert "profile" in result + assert "licenses" in result + assert "categories" in result + + async def test_embargo_options_with_token(self): + result = json.loads( + await get_account_info( + include_profile=False, + include_licenses=False, + include_categories=False, + include_embargo_options=True, + ) + ) + # May be empty list if institution has no custom options — should not be an error. + assert "embargo_options" in result or "errors" in result diff --git a/mcp/tests/test_articles.py b/mcp/tests/test_articles.py new file mode 100644 index 00000000..6e55567a --- /dev/null +++ b/mcp/tests/test_articles.py @@ -0,0 +1,147 @@ +"""Integration tests for article tools.""" + +import json +import os + +import pytest + +from figshare_mcp.tools.articles import get_article, manage_article, search_articles + + +class TestSearchArticles: + """Public article search — no token required.""" + + async def test_basic_search_returns_results(self): + result = json.loads(await search_articles(query="data")) + assert "articles" in result + assert isinstance(result["articles"], list) + assert "count" in result + assert "has_more" in result + + async def test_empty_query_returns_results(self): + result = json.loads(await search_articles()) + assert "articles" in result + + async def test_page_size_is_clamped_to_50(self): + result = json.loads(await search_articles(page_size=999)) + # We asked for 999 but should get at most 50 per page. + assert result["page_size"] == 50 + + async def test_page_size_minimum_is_1(self): + result = json.loads(await search_articles(page_size=0)) + assert result["page_size"] == 1 + + async def test_pagination_metadata(self): + page1 = json.loads(await search_articles(page=1, page_size=3)) + assert page1["page"] == 1 + assert page1["page_size"] == 3 + + async def test_order_direction_desc(self): + result = json.loads(await search_articles(order_direction="desc", page_size=5)) + assert "articles" in result + + async def test_private_search_without_token_returns_error(self, monkeypatch): + monkeypatch.delenv("FIGSHARE_TOKEN", raising=False) + result = json.loads(await search_articles(private=True)) + assert "error" in result + assert "FIGSHARE_TOKEN" in result["error"] + + +@pytest.mark.requires_token +class TestSearchArticlesPrivate: + """Private article search — requires FIGSHARE_TOKEN.""" + + async def test_private_search_returns_results(self): + result = json.loads(await search_articles(private=True)) + assert "articles" in result + assert "error" not in result + + +class TestGetArticle: + """Public article retrieval — no token required for public articles.""" + + async def test_get_nonexistent_article_returns_error(self): + result = json.loads(await get_article(article_id=999999999)) + assert "error" in result + assert "not found" in result["error"].lower() + + async def test_get_article_without_files_or_versions(self): + # Fetch first available article to get a real ID. + search = json.loads(await search_articles(page_size=1)) + if not search["articles"]: + pytest.skip("No public articles available on this instance") + + article_id = search["articles"][0]["id"] + result = json.loads( + await get_article(article_id=article_id, include_files=False, include_versions=False) + ) + assert "article" in result + assert "files" not in result + assert "versions" not in result + + async def test_get_article_with_files_and_versions(self): + search = json.loads(await search_articles(page_size=1)) + if not search["articles"]: + pytest.skip("No public articles available on this instance") + + article_id = search["articles"][0]["id"] + result = json.loads(await get_article(article_id=article_id)) + assert "article" in result + assert "files" in result + assert "versions" in result + + +@pytest.mark.requires_token +@pytest.mark.write +class TestManageArticle: + """Article create/update — requires FIGSHARE_TOKEN, creates data.""" + + async def test_create_article_without_title_returns_error(self): + result = json.loads(await manage_article(action="create")) + assert "error" in result + assert "title" in result["error"] + + async def test_create_article_invalid_action_returns_error(self): + result = json.loads(await manage_article(action="publish", title="Test")) + assert "error" in result + assert "Invalid action" in result["error"] + + async def test_update_article_without_id_returns_error(self): + result = json.loads(await manage_article(action="update", title="Test")) + assert "error" in result + assert "article_id" in result["error"] + + async def test_create_and_update_draft_article(self): + # Create a draft. + create_result = json.loads( + await manage_article( + action="create", + title="MCP Integration Test Article", + description="Created by figshare-mcp integration test — safe to delete.", + tags=["mcp-test", "integration-test"], + ) + ) + assert create_result.get("success") is True, f"Create failed: {create_result}" + assert "article" in create_result + + article_id = ( + create_result["article"].get("id") + or create_result["article"].get("entity_id") + ) + assert article_id, "No article ID returned after create" + + # Update the draft. + update_result = json.loads( + await manage_article( + action="update", + article_id=article_id, + title="MCP Integration Test Article (updated)", + ) + ) + assert update_result.get("success") is True, f"Update failed: {update_result}" + + async def test_create_article_without_token_returns_error(self, monkeypatch): + monkeypatch.delenv("FIGSHARE_TOKEN", raising=False) + result = json.loads(await manage_article(action="create", title="Test")) + assert "error" in result + assert "FIGSHARE_TOKEN" in result["error"] diff --git a/mcp/tests/test_collections.py b/mcp/tests/test_collections.py new file mode 100644 index 00000000..257b5b34 --- /dev/null +++ b/mcp/tests/test_collections.py @@ -0,0 +1,98 @@ +"""Integration tests for collection tools.""" + +import json + +import pytest + +from figshare_mcp.tools.collections import get_collection, manage_collection, search_collections + + +class TestSearchCollections: + async def test_basic_search_returns_results(self): + result = json.loads(await search_collections()) + assert "collections" in result + assert isinstance(result["collections"], list) + + async def test_page_size_clamped(self): + result = json.loads(await search_collections(page_size=999)) + assert result["page_size"] == 50 + + async def test_private_without_token_returns_error(self, monkeypatch): + monkeypatch.delenv("FIGSHARE_TOKEN", raising=False) + result = json.loads(await search_collections(private=True)) + assert "error" in result + assert "FIGSHARE_TOKEN" in result["error"] + + +@pytest.mark.requires_token +class TestSearchCollectionsPrivate: + async def test_private_collections(self): + result = json.loads(await search_collections(private=True)) + assert "collections" in result + assert "error" not in result + + +class TestGetCollection: + async def test_nonexistent_collection_returns_error(self): + result = json.loads(await get_collection(collection_id=999999999)) + assert "error" in result + + async def test_get_collection_with_articles(self): + search = json.loads(await search_collections(page_size=1)) + if not search["collections"]: + pytest.skip("No public collections available on this instance") + + cid = search["collections"][0]["id"] + result = json.loads(await get_collection(collection_id=cid)) + assert "collection" in result + assert "articles" in result + + async def test_get_collection_without_articles(self): + search = json.loads(await search_collections(page_size=1)) + if not search["collections"]: + pytest.skip("No public collections available on this instance") + + cid = search["collections"][0]["id"] + result = json.loads(await get_collection(collection_id=cid, include_articles=False)) + assert "collection" in result + assert "articles" not in result + + +@pytest.mark.requires_token +@pytest.mark.write +class TestManageCollection: + async def test_create_without_title_returns_error(self): + result = json.loads(await manage_collection(action="create")) + assert "error" in result + assert "title" in result["error"] + + async def test_update_without_id_returns_error(self): + result = json.loads(await manage_collection(action="update")) + assert "error" in result + assert "collection_id" in result["error"] + + async def test_create_and_update_collection(self): + create_result = json.loads( + await manage_collection( + action="create", + title="MCP Integration Test Collection", + description="Created by figshare-mcp integration test — safe to delete.", + tags=["mcp-test"], + ) + ) + assert create_result.get("success") is True, f"Create failed: {create_result}" + + cid = ( + create_result["collection"].get("id") + or create_result["collection"].get("entity_id") + ) + assert cid + + update_result = json.loads( + await manage_collection( + action="update", + collection_id=cid, + title="MCP Integration Test Collection (updated)", + ) + ) + assert update_result.get("success") is True, f"Update failed: {update_result}" diff --git a/mcp/tests/test_embargo.py b/mcp/tests/test_embargo.py new file mode 100644 index 00000000..c79c0e38 --- /dev/null +++ b/mcp/tests/test_embargo.py @@ -0,0 +1,120 @@ +"""Integration tests for embargo management tool.""" + +import json + +import pytest + +from figshare_mcp.tools.embargo import manage_embargo + + +class TestManageEmbargoValidation: + """Validation tests — do not require a token or live instance.""" + + async def test_no_token_returns_error(self, monkeypatch): + monkeypatch.delenv("FIGSHARE_TOKEN", raising=False) + result = json.loads(await manage_embargo(action="get", article_id=1)) + assert "error" in result + assert "FIGSHARE_TOKEN" in result["error"] + + async def test_invalid_action_returns_error(self, monkeypatch): + monkeypatch.setenv("FIGSHARE_TOKEN", "fake-token") + result = json.loads(await manage_embargo(action="delete", article_id=1)) + assert "error" in result + assert "Invalid action" in result["error"] + + async def test_set_without_is_embargoed_returns_error(self, monkeypatch): + monkeypatch.setenv("FIGSHARE_TOKEN", "fake-token") + result = json.loads( + await manage_embargo(action="set", article_id=1, embargo_date="2025-12-31", embargo_type="file") + ) + assert "error" in result + assert "is_embargoed" in result["error"] + + async def test_set_without_embargo_date_returns_error(self, monkeypatch): + monkeypatch.setenv("FIGSHARE_TOKEN", "fake-token") + result = json.loads( + await manage_embargo(action="set", article_id=1, is_embargoed=True, embargo_type="file") + ) + assert "error" in result + assert "embargo_date" in result["error"] + + async def test_set_without_embargo_type_returns_error(self, monkeypatch): + monkeypatch.setenv("FIGSHARE_TOKEN", "fake-token") + result = json.loads( + await manage_embargo(action="set", article_id=1, is_embargoed=True, embargo_date="2025-12-31") + ) + assert "error" in result + assert "embargo_type" in result["error"] + + async def test_get_without_article_id_returns_error(self, monkeypatch): + monkeypatch.setenv("FIGSHARE_TOKEN", "fake-token") + result = json.loads(await manage_embargo(action="get")) + assert "error" in result + assert "article_id" in result["error"] + + +@pytest.mark.requires_token +class TestManageEmbargoLive: + """Live embargo tests against the local Figshare instance.""" + + async def test_get_embargo_options(self): + result = json.loads(await manage_embargo(action="options")) + # Personal accounts (not belonging to an institution) get a 400 from this endpoint. + # Both are valid outcomes — we just verify no unexpected exception is raised. + assert "embargo_options" in result or "error" in result + + async def test_get_nonexistent_article_embargo_returns_error(self): + result = json.loads(await manage_embargo(action="get", article_id=999999999)) + assert "error" in result + + +@pytest.mark.requires_token +@pytest.mark.write +class TestManageEmbargoWriteLive: + """End-to-end embargo lifecycle — creates a draft article, applies and removes embargo.""" + + async def test_embargo_lifecycle(self): + from figshare_mcp.tools.articles import manage_article + + # 1. Create a test draft article. + create_result = json.loads( + await manage_article( + action="create", + title="MCP Embargo Lifecycle Test", + description="Created by figshare-mcp embargo test — safe to delete.", + ) + ) + assert create_result.get("success"), f"Article create failed: {create_result}" + article_id = ( + create_result["article"].get("id") + or create_result["article"].get("entity_id") + ) + assert article_id, "No article ID returned" + + # 2. Fetch embargo options to get a valid type (may not be available for personal accounts). + options_result = json.loads(await manage_embargo(action="options")) + embargo_options = options_result.get("embargo_options", []) + embargo_type = embargo_options[0].get("type", "file") if embargo_options else "file" + + # 3. Set embargo — use a date within the API's 25-year limit. + set_result = json.loads( + await manage_embargo( + action="set", + article_id=article_id, + is_embargoed=True, + embargo_date="2030-12-31", + embargo_type=embargo_type, + embargo_title="Test embargo", + embargo_reason="Integration test", + ) + ) + assert set_result.get("success"), f"Embargo set failed: {set_result}" + assert set_result["embargo"].get("is_embargoed") is True + + # 4. Get embargo to verify. + get_result = json.loads(await manage_embargo(action="get", article_id=article_id)) + assert get_result["embargo"].get("is_embargoed") is True + + # 5. Remove embargo. + remove_result = json.loads(await manage_embargo(action="remove", article_id=article_id)) + assert remove_result.get("success"), f"Embargo remove failed: {remove_result}" diff --git a/mcp/tests/test_projects.py b/mcp/tests/test_projects.py new file mode 100644 index 00000000..51f5c78b --- /dev/null +++ b/mcp/tests/test_projects.py @@ -0,0 +1,46 @@ +"""Integration tests for project tools.""" + +import json + +import pytest + +from figshare_mcp.tools.projects import get_projects + + +class TestGetProjects: + async def test_list_public_projects(self): + result = json.loads(await get_projects()) + assert "projects" in result + assert isinstance(result["projects"], list) + + async def test_page_size_clamped(self): + result = json.loads(await get_projects(page_size=999)) + assert result["page_size"] == 50 + + async def test_nonexistent_project_returns_error(self): + result = json.loads(await get_projects(project_id=999999999)) + assert "error" in result + + async def test_private_without_token_returns_error(self, monkeypatch): + monkeypatch.delenv("FIGSHARE_TOKEN", raising=False) + result = json.loads(await get_projects(private=True)) + assert "error" in result + assert "FIGSHARE_TOKEN" in result["error"] + + async def test_get_specific_public_project(self): + listing = json.loads(await get_projects(page_size=1)) + if not listing["projects"]: + pytest.skip("No public projects available on this instance") + + pid = listing["projects"][0]["id"] + result = json.loads(await get_projects(project_id=pid)) + assert "project" in result + assert result["project"]["id"] == pid + + +@pytest.mark.requires_token +class TestGetProjectsPrivate: + async def test_list_private_projects(self): + result = json.loads(await get_projects(private=True)) + assert "projects" in result + assert "error" not in result From a3a5847a943b6c0e7b86e634bebeefeeb2366774 Mon Sep 17 00:00:00 2001 From: Corneliu Date: Tue, 14 Apr 2026 14:51:18 +0300 Subject: [PATCH 2/3] Add .gitignore for Python artifacts, remove pycache from tracking Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 874636bb..4165a9fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ -.idea/ \ No newline at end of file +.idea/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.venv/ +dist/ +build/ From 8d5453dc57dcc465cf260c81fda3de2fd20a523b Mon Sep 17 00:00:00 2001 From: Corneliu Date: Tue, 14 Apr 2026 14:51:25 +0300 Subject: [PATCH 3/3] Remove tracked pycache files Co-Authored-By: Claude Sonnet 4.6 --- .../__pycache__/__init__.cpython-314.pyc | Bin 309 -> 0 bytes .../__pycache__/client.cpython-314.pyc | Bin 10023 -> 0 bytes .../__pycache__/server.cpython-314.pyc | Bin 2635 -> 0 bytes .../tools/__pycache__/__init__.cpython-314.pyc | Bin 248 -> 0 bytes .../tools/__pycache__/account.cpython-314.pyc | Bin 3334 -> 0 bytes .../tools/__pycache__/articles.cpython-314.pyc | Bin 12239 -> 0 bytes .../__pycache__/collections.cpython-314.pyc | Bin 10017 -> 0 bytes .../tools/__pycache__/embargo.cpython-314.pyc | Bin 6276 -> 0 bytes .../tools/__pycache__/projects.cpython-314.pyc | Bin 4434 -> 0 bytes mcp/tests/__pycache__/__init__.cpython-314.pyc | Bin 293 -> 0 bytes .../conftest.cpython-314-pytest-9.0.3.pyc | Bin 2493 -> 0 bytes .../test_account.cpython-314-pytest-9.0.3.pyc | Bin 14186 -> 0 bytes .../test_articles.cpython-314-pytest-9.0.3.pyc | Bin 27912 -> 0 bytes ...est_collections.cpython-314-pytest-9.0.3.pyc | Bin 17804 -> 0 bytes .../test_embargo.cpython-314-pytest-9.0.3.pyc | Bin 18965 -> 0 bytes .../test_projects.cpython-314-pytest-9.0.3.pyc | Bin 9489 -> 0 bytes 16 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 mcp/figshare_mcp/__pycache__/__init__.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/__pycache__/client.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/__pycache__/server.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/tools/__pycache__/__init__.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/tools/__pycache__/account.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/tools/__pycache__/articles.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/tools/__pycache__/collections.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/tools/__pycache__/embargo.cpython-314.pyc delete mode 100644 mcp/figshare_mcp/tools/__pycache__/projects.cpython-314.pyc delete mode 100644 mcp/tests/__pycache__/__init__.cpython-314.pyc delete mode 100644 mcp/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc delete mode 100644 mcp/tests/__pycache__/test_account.cpython-314-pytest-9.0.3.pyc delete mode 100644 mcp/tests/__pycache__/test_articles.cpython-314-pytest-9.0.3.pyc delete mode 100644 mcp/tests/__pycache__/test_collections.cpython-314-pytest-9.0.3.pyc delete mode 100644 mcp/tests/__pycache__/test_embargo.cpython-314-pytest-9.0.3.pyc delete mode 100644 mcp/tests/__pycache__/test_projects.cpython-314-pytest-9.0.3.pyc diff --git a/mcp/figshare_mcp/__pycache__/__init__.cpython-314.pyc b/mcp/figshare_mcp/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 3af283a1956eb05e3a409d2c4bb0ac200a90d87d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 309 zcmdPq^Md_YD6Ll8p=Ll9#LV-S-vgC=vSEl|)cGrc$? zu_#r+*Ev9;IJKxOwMgMn!xV+oih}&&)M5p=ykmf;LYa|5Vlh}{Nq&A#v0fFcfu5nB zfuAPRE%x~Ml>FrQ_*>lZ@jx?*GxPJ}<5x0#207qXntmwI=wkik{GzgOgGz~$pXocQ?6yv&mL uc)fzkTO2mI`6;D2sdh!|Kx;vsFXjajAD9^#8E-N;Kj0H>;x1wZiU0r)om-dy diff --git a/mcp/figshare_mcp/__pycache__/client.cpython-314.pyc b/mcp/figshare_mcp/__pycache__/client.cpython-314.pyc deleted file mode 100644 index 55b61f70a03c6509f775d4989baf29092454676e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10023 zcmcgyTTmQVdOkh(8w^*0jDVIvvPKsMA!#KGfffmLMF=$`Ur9`cZfY?bA;tMrhE!FUVYwVTym9Sjf~ehb*E(4UC*}ZZpop0B&Xz(+>%G~NLEQn^$iRZ3M-wX{yEk!r&}-FpEuToxLcw7!N(Pt>qvneMA*8<|vx?+trw=8kl~ zRFCfl-X4&ICKeL2U!u)-JQC>_<4HwM>EfldDjrpawUL-Ai(UOE#Id#(pHGrw2~CWr zQXTNVp7%)_(bvOiDLuDyQH3IHRB2{cn$d_W-q<%mg(e;dWARLZmloo89pQ$CD9u1GRg?xP> z!LWCw#wY3?(M|bRJ7daKfw76|!?A=&8g%IYJ4f)dX| z2dy{|9XdWD$1jWH=`0|-5-Mv!Px%3@*3)GIC?Tg5IdQ-oZ9*%NmNhYz*2R(7m`o5I zRWyww1gO(T6irN{lJ7!!gXXG`WDSeQWj^W>fSORQ^M)fpXi|w6pXYC=;fcXmQc0Kx zkySOV79IF~bjY^gz!H!oCY4bID7q4tc{Gz&L0HLgF`J5w0jfk|;?}*bZ6Rgpw;i)P6vq%o4<#2K zAeb$lL^jC=*|WC&FEf@v-3E467!f342rXukY(sfY!rARR+*Uol977K4D{Kc}Ie$Yv z4Zo(x^sE+*rxUVKqRLt(ozmp!#dKo4&v2=-o>fyJyI}`$8;%6 ziCm1EEf%e{Ow^o9$yYLRTnBfWZCiOiLkJnnaF0R?$A)FYWy&+d$@@%rEcq4^RP36C z97ZtOBS~kZ=;@w;fv#gc1BQKA)>YDjs-O2z>F_Gy+wc(Ea<-?z8ZpO~EuyFbOw6c? zUd$@VM3k=-&2K=hMH45uzeHh@{W{n%7u+-x+%(;uYddjg`>EE zBsh8W=kBVzf%S8N`k6reHDxxib&1(aHZ4##AKWk(6la3sbnR?#+mhX?zgwuUnGJ@P zoL2pP7b~m0+A$m4xZq{p;6i}4?ElC8pY1#{wg1&auN<1*GIeOyAIdq--l;{VmSTr- zJcVRY^5Vatid6muD0(P++q12@eH}}Ks&&U8+bu{oC|{3c=PKBF!Nuzwyv|8=d-7B2 z?h78?=HhMc6>VN=9dGmUHXpSm1>Gmr@LE5w4diS6`MxE*wlrTG$oCEM+Om9YNxrt6 z*H+|fOZlvoysnDs^q^F&mklz#T=IZQYmHJ%W*^}e#KfM+2!wu28&Ac>7t~k=%1Df< zq;x@CvLeJUy~S!pzq(&w%g@lq}_8QZ*(M z8FoO3a}uOJ3HCfUsr4v7#6v1EX9u;^A>+K_cUInG$lk47pw~_FYk7<4#}g4NfE!r6 zivr2Z0oHGApLZ~`Wfmxq#=(N9H2l0uPY@G0b8wvV+1V7-*9$!j7(IEF%oq)+7g*a3qN=q+DT>&PH|$ z)W^{XWw6(-2%uS-m%KY40H*5N3yMAh9=El$8p4RW3ypcsHWh;5Jvc0rjih!Odo5_2 zs;p%V3DR>^a<~-wtX$F_}ew%rWgLerF*f6VUMN8 z4LgtnF6E~o*12RVx2@D+)8kKZ5dE|$GVtv}84Cno?R=&4T4>JS{HeeBgMq&vxSv18 zq8+FCVBmv+zgtlU@`bGX^wNmMxqjH}JPjX`@?Vry zE;#w8iv=qd+`Q;v<<(d9ANm)({L{yRl~*I*zr5h*p8@78xmQ9(6NLqf2eT85ULu8Z z1KToX$=XN^!2Kc_l5s}tMJ^&&3ymzy)^aK>MN-Djxz4$>_AtxZj>12F0c)wlgYHXw zG%1zy!xwF*f~Gvmcrvbxd)e0OFIZUEYd*mmgf%8P_7O0V+J&e+Ys=cBHfzme@VcMF z{8xlYd#}^SGTw-HEn6@mM4UJ=!C_U3cpg3{oNU;A`|=Px2KQve8a@fDLakw4O2=#~ z+Z)-_zO`&YOGzd+lad_t^^>2pDp`F@MH~i0p&xO=s$`uXcBPQ<*FC0J#yRE3@VP+4msaz8;_FR$US!j97d(X!g@lCNt ziVtzM3SQLUzn>g9($|B_fU{dBuyti6f4Mk*djusIQ|(4iu5k=^-gH)vQq?hJp%OKO zm*G_4`fG-h)Y=up-$h=asZ3#Bo552qISrqpMYSvsavE*~mdO*~Ga23Sw zE)TTY*(Z_F?W%xNHbxMj(zzWGZS3GQ&YzGCbDNNtqChe zKdC%%Co*5Z@tvXTL-RHD^Hnu)8{R3oUNT?PG*`1dSF?RupRZoO;Aain7MZJ|bcwlY zOQ()6*;&;|0TV1#ut3$EzXAU_f5Y9%y1B|G{O2m0=4;kp_0Ct;z1i_v#}5wW0-K=0 z%c`av3pTsAyih;Yec!`^b)VIRZ+YfgyJuRvKTOQlotiD}&3SqsEbd~plJJYl6ZdRb zwy-GH{j~DLf8O81%GTd!g17whK-q#F@Bh-?0ia)O>8TKIdAirTe&j-S+x{fJZ*LR2 z8$GwT+o{~r(!JUF(FUP=qvxY~2bDMaQUB2np}WcP(avg=e{3(Oyt<0=ZDlv@M1S)TKb$5C{-Y#^vlzzO!N9BDs%AfL}^%KG06SRL4uu-{GpmLB_U!itM zT$2D?Jb>)6YFAht5a!C2hk{KDra+~AT$QSHWAJ$OnME&c3o@=g1?M3$VGdnK02Z&C zdu}zq-_DD-5q?FDsTlKO8>7tV>q``zAl@w~M#~oCUCc~k=PM*(Uw$l9f~{C3G?7?z zaG`~8^|77^3?E{-zMF#j4BjwJ$qrHrr!+0>Ev8MR!fRqi#VTtE{yRS&xsk*~pyoF( zymn!F?~UfG7iOzF=YpL%Pv=aq^T8r9>5Da@a9wC{yyX;EBFckCjwoWu{}rNEBaf^l zTi5~1$W??ql5%~fpmC%44Ka8HUA6tl@)$e?14wY560U26xKqN-=37qoZ{67QVT5peaxQo>=Q%kOJo$hN;umW-3U3LG zj=ykjT#4iVBWAqmdGJ<9CAyXMb1Z$Z<7^y_twmM`@cRnP6R*W`z@;ReZ## zTFod_%MZ000sC%r6Q3NM3m(jQ4$cG*u7LnzZZ7`_B9E7UMe#mUToVU?@5y!ehECo0 z&~-WJCpdQ#{C88(4=)P(;bj6n@7JKe87Jpr>o7hZEw&KZ&W~K}n-nP`o8jv2>FtU1 z4=q%L}Jxfe>*jHj3L*!xxQ?;^r<%W!$SipN%E+Ex~B?l7awr zQc20FG@9MfD6VL6#Bl~psZn$@}ldIxSUKzqfp>Hs!OK8x6Rnm6Eq8Dt#~vN2w(jP^z!G9 z&LsyfKo;m01XiW1V#$W!?xG!~1u88(!Ca+FHiTHHu5^J)OD=NMzu><9Ql9|Z zQ4RM3RhQ`PpHOH!?O6y zt{vj(uBW5@xS5R(ocR5o3oZ1+g3;I&WJ<~?S<#52zKqq2!&X6DmyKrND_M|v1s(XY z$#}zktiN>X`8S%bD(}_LR)lB$tvN^Q@{z5hInbBOB2p7;p(l)ADlH?0Uy9d}cf1_Z zUyhgzk=EyFw`~UjKcCuR(vYkJc=F*va{h5cT?Z;pwULuNm+G7ErzDqY|FY zn~g^xn+~H9dlRx|O51?9AyAnm}$Duc(2=&@G>s@b%BGIhm@ywgw`~K~-&z28ZfX_QK zzq|8!fIsyid9v|l`|S+C0GgoSHZ+ZfQO~H%oZe>|*}AFBXrFCN)N?9V&#QdBpbC-9 zY)sZIWz~zSSTCtkB%5d)sFzha+UFVv>r-lK4%RZKp_#v6j6AC8wdvJ2k}%Tm3-R_e ztQ8V0>b?q_+Xr?W!~d{b+J{|hAK2nPuv7cMn)|?(Q*5(v6q=Jq!JATt*N!x;OJ4xA zkTtWgcK9?TJ(+`MaX$J~NA&PTP60WgoBUV){mlGwZFMC|5%N9X670!lD1*&*xKtF#Beu(9y7p!|| ziwn{dZl5Yf5+v$&m`W5Qy+jw~35>LuqOKkwxZoT zf@x>yH|Vt@x1=R#JGFbp=cC}{MkP(d;hx7FrH#KLjUC81lTa?Z8p}88*P4s=%}+mCTp7oU+~7X- z2z4ALC9?P{=94pmeI2-hwMaXqV(lB@YL4{S$eq+g7+d}D+Kol~R`cVC&DT~NWSvSz zwBSsOwQ1m~%Va~Tp1fSE(Vknyu%maZ>hNx@f4*W>j4;1MrNVa(w^O<|7}L1Gm26my zWTQ7HEWb?U@ZhdD3-M)*PrDyZ$0|EShhnW%B++?dJ8lX}O4?LHm9UTm@941~kAA?P z8qhyH+q1X~;B&a0x%3bR4$5ZXGh@SO8qG}2n1@RCAB_!jf~i3GD?b8y$YG_sR7q{^ z^{`dDg>y&N94>t3xj_xvDr!CFm@N9sx?6J{}~ zFe?IIOrcA0L@$SM3C%8beKCXEC@pI_#)6I(wgXH3N3luW+TyTY<-}3jL2p zw@&_q%k!*Z7+aZX!+h~37;pUz7oWk+Cvfu_+<5|bp2E9N;lfr9^5xCDKj)4Qvd?p1 zn2!n%3JdTFm$O4SnO^MASDe9K@24fL5#`_noLzvZkg%D8Hq)y3ck(( z3MKjZISRS?DWy57#d?04jJMe1<5TjJA^Qxv&LkO8M@4}EXBOMTRJ z?E!c8?VC5hH}iYL_og=*2@+_D#9!2Z1_=2Zet3;{g`Iv7=1GBA;6MmdXvw{O&U ziMRMRQV(RS z3e(hDmQ`lW?35drjHWBHmMO8>tih`Ko`D+7%9M1aRtCPDfMv@{wK1`X9~fsd>jtjB zdfN^ci*lu+gFP!2jVLT20*^Ta-8^|lPIJB6*lji++2Y~-#T$t&0p3qVawNGWp%fs; zJGPoo3KFqtI|R}W%qEH-d$$Sj{xKo`o%0Z*4$=nT6J9jQTdq;npCwfR9&wy>ySmah zF{b1GYmNB01Mu=}rX{o~aN}aZcM|khl1Fg{`aIAI{9CTqcj3=)1J-hjo98bJK{77X z=ZZHTW2F z{jc$g5l`Dr-P!!OOK?o@`|7GPC58Y{^3uGs12zmS<&ElP5Lyf~bwk1RGX0llBDPGYT#z}W9{bp{b-F?Sn@5sqL*(Yv{{?H!4%8m z1}flwumeS6KjNM;nbDxMYK>y4=oP35HN(mD&)ItrP6*T*m)-ZSe{hvTwL$uSs9G}) ztH>8Kku^jgY=X&#_?_R%jv5+f{eeiznDuv_Va$N$h=@he+87~~RnQn?>g&**si#>~I0@Lu#4jaDownd}aG@n0mz)O0{y;Oa%&}LG(4mRR$V)O@K$BCf!Zm^csKYGs^-&^BzkNu zlAO2G|NzL&oLy|wh)>s@az zhzqw@R8w00*m;QBn<(#rMP+IRREr=^EJ*)8) zPK<6GZNGDCHPHG+DE70VpAIcd{ZjdJD81rOZ=WG2E^=RO9%_3^xJYKRtNT~{-AlOU zC-L@wZSkP{dOJuuE_}5aI|?RE>CN`8kFVct#brMUA4ij>G@5h`15DkfZ?p5{$L8JB zxb!Ds>e%#sy&WeR)^Ihr*_XY~e+sT(nNQ*=$JgM$%@FYZqn><{`&5MPH;0EWkVTOj zKIdPQB$T5Ps25ufk93p8UT)-sf3Z(O`AtWE_HZsn9>lp^#Q)$m3FTJQKcKH+KY)-A z&K%C2A`c^6ZqWZQDxutrHV=~&`*R$4d3Yf-(!nnUqa#UvsZ{{|Qc^%o2aoaz4m>V( zhjKxFX&{;t_+^4N%Yp!!Mkna-HfA1!4&ugf+dhHw??ed~{yXV^Cc;h9xqLda{{vg0Q=R|- diff --git a/mcp/figshare_mcp/tools/__pycache__/articles.cpython-314.pyc b/mcp/figshare_mcp/tools/__pycache__/articles.cpython-314.pyc deleted file mode 100644 index 3674ede2b310c22330ef74cb86cd1ae2990c7ee6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12239 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%0D*gH1`!|Da2c z@pU%L-_khLLS1*$L(Tf@HD-)AnIQjqN8_-bx;{t`>-5*RnjwCJX@UG3?KITAvC1~& zWp4DcL$%CJI}P!h4g-|ltTkZV%V4~j25mQ6Y(v|bn_bSKLFVQrA~R^fob4!cOGks| zTYB4Y7jw(y9PVIlwW7?e4g=Q`igCQNlCRm ze{mgS<%p})?!_J{&k{{l2UrD(Jwe`Fr5~V#{5(P4Qqj7k;T zitRWnb%G8+l`a`o8e_y^KsJzl0(zb$8`?#LB-u^{95TU&0lH%(6I?3b(;_d~t6ggX z@G5VWG(yTfREF@{AY_1=O=lpYtkFCvR8_Ba%HTCZve9tmJ^s$>Q zJ4l`rpzOPy=Z?;Ie(Y!~yGWj!a@NdkJO|DOlB}gX4fFctfQ=s+1$D2ay>~l zsKQ>7Y@}R3zRsN}H<4sB;$t61Wq_>NUEwVerhgX!@)cMuD*|M&xBo8yVv1{f7FK5h zjwtX+JRMFO5GHfR^-Kl2tp?9A$87mFXjw&oy9QV%WlzV`wVIY_(-Uo^PZ4_Jn`*IC zYYBo6BBE0~0ey;ZgHh7kWEdRLE9t!)fp`&vl0L>c5b|lhlquPh5HTs`-{D-O+^nR} zac+`UeJ6jx)sVF6iTGQtmZWWp{6BFXOas+WM-EPPpgi{FhzC~}aa1h#Korc2$cOW( zU#)@ja*bRQ*Bo&MnF@I4d=+m1w5-8%=$P_wEfE^&(UaPT`yBQIF11GGSFNl12OjIU z>l^mF75eR3so&sFcnkiA;J*hTJ#Js*(TIursjbjsh<(Tt*1Ri~nMAx9=B#_W8;A+{ ztq>Ce{M++JnBY5By7{ye!Uw<>PAs8-!z*3@0ImSf!IyxyyAY$}H6idT03y_^-XbxC zCjr^x{R#Q1l^Em`NwBLEX9F0#!x>e)mz_lS1VP|hA5Qb*fQ`YnJrRrZi`fS`1;98i z>+Qg*%5AO~^y9SLLND7PU+f~~*fcKO8k8b}tmZPx>n7OK;PPPQ(*_Nu zs-^%NMv@?N5^Pp(K~_XAf&e8^OYvTaHiD0?8IIM2S(fj3n;;9~>=fsXHBx8CSTCLm*A{f6(z%__$*DkSGI zvG=17i9mMQDjrRyC&(Nid@yzjZu7;)frct|Mdb~v4{~5}ng>Dg0pPtp*_WYs4?18L zOc0j>##BE7#CEXApP1zFa-HI!kjFwr8(_1;TMRf9&;aPn#FOA)lS!yD zgV3Y#RF4O;R(UEVg7R#vy2d2mio}nCQy;DJ-~bUt@D-@ca`Tg!rLwG`o_a}A?wbfU zc(RK~M!8_xJ(@lhOC_OkvO3wW>6Weli>`?-S+Wbe(IrcF1*ZLExh(RwELn7Fy?xVx z${KxQ1w}bQhC&3k$t8$TIE;C4_)`P~3P&(@6oToR$2hXE3GyYAn2*O{#KOWMl2k@o zrl-hxg}o$SZaLxC35Z;UV-WZ)%d+9^SWa9cOg5jZkYn(MU=uPq#*3!N_)n#YQ+V+Hdukeh3~Yij!Gnu=iO{zCJC zqU)uC`6ab@{=}WO{wj%Zq4`kJ6)Bh_Wh2$nHZxQ*+3%PdOO36whtE1nrrJBEwo;pa z_Sk~iS2Ee|n7k#gZ#Ht)UNY6(F}0RjyJp80%*`c}^Ny*x)Vyl;$XVBtgy)W_z0|&L z_LT*5OUYz?>&;R_)9j(Mwk2Y&J0@Srw|e&I0x)>J4J-n-nz?V^wY8RftMD3Gp$mN@ zTZ_KXz538>NOen`x!ifheAR#LNZ~-F(02H4EOFPv7wkN|j;HB-&pSPZ_5rjO6k4CZ z7M|-V*6zAz-&L^h`V?F~yDsc1__to&b@jPI*Un76V9r=rmy5!$90`lc>-aFbSzI(0nV@K>*jEf$8nT$IpbrA-|2pRq7lM+q< diff --git a/mcp/figshare_mcp/tools/__pycache__/collections.cpython-314.pyc b/mcp/figshare_mcp/tools/__pycache__/collections.cpython-314.pyc deleted file mode 100644 index f167066072ed4209c8c0252236773b03864d8102..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10017 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 Q6217yMc-l7XQ3tHzu?3+9RL6T diff --git a/mcp/tests/__pycache__/__init__.cpython-314.pyc b/mcp/tests/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 6f9e0a4d95a625997011fc834ae41da1e0854783..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 293 zcmYjM%}N6?5KgRUDd+=u8t_!gHaAb=QSc&Ika`OtJ6!|2lXa35y!9b`314Xi5B3Fw zzJS@KEzV(nznSlw+2zGVWV^Zgb}u<@`{H17pX_?bMl3`W3wbT3C+R6S9w=hPCOS_L zF~k5X&#-b;cr}bgV_W#yd;sHHa*SYr6Op3|YHv*q_j@2X3ek|o`Qub}=USKEYW*p8 zJ$a|qb3hKtdM2!0Qb_BdjwHjWqQtdgZ2iAM%3DRglp3TsXjEB{8gweH9l5CWynXM+ bw~a4Tjdy&O`TCE;IJ@7Gl=4d~`7{@QDo>FFJBBvrrW}v&q*!b6z5qfm$S%Mp+`v-6MxmCf!`!P z$EyzYoaF{#c$t6}f@$dco?n~GVf@MEOV<`ISo8MhpIw<>#87^3Z{ilAEbyHMc3hV* zhIw$CC<`1cO@s9y1l3>{uM^w|TsZ9cj61$d`qf^%aBbecZhb1%l2WaN>zudd%4I21 z4;Y_2GjnF9y!rtcANu%?$Lp`#b#85Kt!HYY^W2+M;g)hBc&RKsQ^v*B+@c%9Cb1Gk zNg-tHEW)6TZlFs4ZVacTS^BK5CKGQylj@zZ41jT9q8eUii7O`@dbP}>+m8_O2 zX$5^7AxIUgfg1|CNw$`It`|255K67}XjB1XC(I@^4Hxf$%8oe*-I3-H;G(tzv%@|#TDu!C`Zz0_E zSLJF4rJOKamP#fW^?Hy?C2|vxGA6qfxG{U}w%1Z2RgB@TO7I!>Rl{*`-IItMCG^H-mz(C9QYnfIwF z?l8eR2B{G4A7F0KB9aD4*a{#86?TrxyKbN*{mfl}8P<-Vt6E*Nw41P_N*S{41N|={ z6QwEGN`=-G<5UC%B*~`3<@1iI=jCnN@xhDFyY#%gj^ylEv{j>MZcJ{s3nY#3NaG_%? zs8;D%I3J(cncck(4z2TadTF7uX|IFPI~jal(?DL z_wNCXlE6%Q@UBM1A4MTWF?2n@(RajDa8J1|TX*7YO^?CiBvdBoEqph!ssE7uKHE7o z^>}pp@!-jhaZ*Yah9lFqAvxN1lvxhF28Wy+<T7(VqF`Wh};XAej&2~!D zwC8$OGhXfo@83n=WdA@zFVZQ^)?VZdh@kCZq-VYz|9bp8?}>*0)&=o*Qb<3#Er;8g PSGS9D_XX@0r91ush&V+e diff --git a/mcp/tests/__pycache__/test_account.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_account.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 0737c58276f42f4def82022f1b739ed494aee0e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14186 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 HfX(#3`Lk2z diff --git a/mcp/tests/__pycache__/test_articles.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_articles.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 035fff42b7f8d6568102784d53ec4754bf902e63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27912 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 diff --git a/mcp/tests/__pycache__/test_collections.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_collections.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index b92d1230a36bd8407b59191d65f354160d371a6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17804 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 diff --git a/mcp/tests/__pycache__/test_projects.cpython-314-pytest-9.0.3.pyc b/mcp/tests/__pycache__/test_projects.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 73057fdc7abf22be520774172f8febfcbc1cb1db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9489 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