{
  "$schema": "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json",
  "version": "1.0",
  "protocolVersion": "2025-06-18",
  "serverInfo": {
    "name": "syntology",
    "title": "Syntology",
    "version": "1.0.0"
  },
  "description": "A provenance-tagged knowledge graph of AI/ML research -- papers, authors, citations, methods, and code. Every claim traces to the edge(s) it came from.",
  "websiteUrl": "https://syntology.ai",
  "attribution": {
    "summary": "Paper-to-code links served by this server are derived in part from the archived Papers with Code dataset (last public snapshot, 2025-07-28), used under CC BY-SA 4.0 and modified.",
    "url": "https://syntology.ai/attribution"
  },
  "transport": {
    "type": "streamable-http",
    "endpoint": "https://syntology.ai/mcp"
  },
  "authentication": {
    "required": true,
    "type": "oauth2",
    "description": "Bearer token via OAuth 2.0 / RFC 9728 protected-resource discovery. A trial token requires no signup -- see websiteUrl/developers.",
    "metadataUrl": "https://syntology.ai/.well-known/oauth-protected-resource/mcp"
  },
  "tools": [
    {
      "name": "assembly",
      "title": "Assembly",
      "description": "A paper's CLAIMED METHOD as runnable, typed, swappable code: the entry brick, its component bricks (each with a role -- model, loss, update_rule, sampler, ... -- and a measured tensor contract where one exists), the wiring between them, the assembly's own verification level, and the judged claim match (IMPLEMENTS / COMPONENT / UNCLEAR) with its evidence. Without with_code this is the map -- free to plan a swap from; with_code=true returns the bricks' source and is a traversal. swappable_slots lists the roles a compatible brick can replace. A level with no verification_report counts as 0. Accepts `arxiv_id` or `id_or_title` -- the same argument.",
      "inputSchema": {
        "properties": {
          "arxiv_id": {
            "default": null,
            "title": "Arxiv Id",
            "type": "string"
          },
          "with_code": {
            "default": false,
            "title": "With Code",
            "type": "boolean"
          },
          "id_or_title": {
            "default": null,
            "title": "Id Or Title",
            "type": "string"
          }
        },
        "type": "object",
        "title": "assemblyArguments"
      }
    },
    {
      "name": "compose",
      "title": "Compose",
      "description": "What fits with a verified implementation, from the typed tensor contracts -- the only tool here that is not retrieval. Given a routine's OUTPUT contract, returns other verified routines that accept it as an INPUT (direction='downstream'), or that produce what it needs (direction='upstream'). Two confidence tiers, reported separately and never blended. 'typed' means the output's role and dtype family match the input's directly. 'adapter' means reachable via one declared deterministic framework operation, named in adapter_op (autograd, softmax, flatten, ...) -- a real composition, but the caller must apply that op between the two calls. A third structural tier exists internally and is deliberately not served: it matches UNTYPED on kind+dtype alone, at 33% density over all ordered pairs, making it a recall pool rather than a candidate set. min_level applies to the PARTNER, not the queried routine: composing a verified routine with an unverified one yields an unverified pipeline. A level with no verification_report counts as 0, as everywhere else.",
      "inputSchema": {
        "properties": {
          "name": {
            "default": null,
            "title": "Name",
            "type": "string"
          },
          "direction": {
            "default": "downstream",
            "title": "Direction",
            "type": "string"
          },
          "min_level": {
            "default": 2,
            "title": "Min Level",
            "type": "integer"
          },
          "limit": {
            "default": 25,
            "title": "Limit",
            "type": "integer"
          },
          "code_sha256": {
            "default": null,
            "title": "Code Sha256",
            "type": "string"
          }
        },
        "type": "object",
        "title": "composeArguments"
      }
    },
    {
      "name": "explore_paths",
      "title": "Explore Paths",
      "description": "EXPLORATORY graph trace for multi-hop questions no other tool answers (e.g. \"which venues publish the papers citing the paper that proposed X\", \"what concepts do the references of 2010.11929 discuss\"). Returns a traversal RECORD, not an answer: resolved seeds, every kept edge with provenance/confidence/score/hop, and where hub fan-out was sampled. Use it to find candidate nodes, then confirm with get_paper / get_citation_path / get_concepts_for_paper. Calibration, stated plainly: on an internal 204-question bench the trace reaches the right neighbourhood 42% of the time; a thin or empty result is a real limit, not evidence of absence. Name an arXiv id or quote an entity name exactly; max_hops <= 3, beam_width <= 8. No LLM call.",
      "inputSchema": {
        "properties": {
          "question": {
            "title": "Question",
            "type": "string"
          },
          "max_hops": {
            "default": 3,
            "title": "Max Hops",
            "type": "integer"
          },
          "beam_width": {
            "default": 6,
            "title": "Beam Width",
            "type": "integer"
          }
        },
        "required": [
          "question"
        ],
        "type": "object",
        "title": "explore_pathsArguments"
      }
    },
    {
      "name": "get_citation_path",
      "title": "Get Citation Path",
      "description": "Find the shortest citation-graph connection between two papers (each identified by arXiv ID or title), bounded to 6 hops, CITES treated as undirected. A null path means no connection was found within that bound -- a real absence within the bound, not proof the papers are unrelated (see get_citation_path's own template docstring in templates.py for the honest caveat on what this claim does and doesn't mean). Returns {\"error\": ...} if either paper can't be resolved.",
      "inputSchema": {
        "properties": {
          "paper_1": {
            "title": "Paper 1",
            "type": "string"
          },
          "paper_2": {
            "title": "Paper 2",
            "type": "string"
          }
        },
        "required": [
          "paper_1",
          "paper_2"
        ],
        "type": "object",
        "title": "get_citation_pathArguments"
      }
    },
    {
      "name": "get_code",
      "title": "Get Code",
      "description": "Fetch ONE stored code sample by its code_sha256 -- the identity every other code tool here now hands you. get_reference_implementation, list_reference_implementations, compose (in resolved_to, partners and candidates) and assembly (in components and swappable_slots) all emit a code_sha256; this is how you redeem one for the actual source. Use it when a name is ambiguous (an `entry` is a display label that hundreds of samples share, so compose answers an ambiguous name with candidate shas rather than guessing), when you want the exact body an assembly slot holds, or when you already fetched a listing and now want one row's code without re-running the search. Returns the source, the entry symbol, language, source_kind (generated = a reconstruction of the paper's pseudocode; harvested = the authors' own code), the origin arXiv id and how it was established, and the verification level under the same rule as everywhere else: recomputed from the verification report's own evidence, and 0 whenever no report backs it -- `verification_report_present` tells you which of those two a 0 is. It also returns the two judgements that never involved the paper, each null when the sample carries no reading: `behaviour` (this sample's fingerprint on a shared battery of inputs, with the battery's own shuffled null and lift, so you can weigh it) and `metamorphic` (which properties held under input transformations, and which failed). Neither is a verification level and neither is folded into one. An unknown sha returns {\"refused\": \"unknown_sha\"} -- a fact about that exact body, asserting nothing about the method or paper you want; have(<name>) answers coverage for free.",
      "inputSchema": {
        "properties": {
          "code_sha256": {
            "title": "Code Sha256",
            "type": "string"
          }
        },
        "required": [
          "code_sha256"
        ],
        "type": "object",
        "title": "get_codeArguments"
      }
    },
    {
      "name": "get_code_for_method",
      "title": "Get Code For Method",
      "description": "Get code implementing a given method or dataset by name, via the paper(s) that propose it. If more than one distinct method/dataset shares the same name (a real, confirmed occurrence in this corpus, e.g. multiple unrelated papers each independently naming something \"DASH\"), results from every one are returned together, each attributed to its own origin paper. Returns {\"error\": ...} if no match exists.",
      "inputSchema": {
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object",
        "title": "get_code_for_methodArguments"
      }
    },
    {
      "name": "get_code_for_paper",
      "title": "Get Code For Paper",
      "description": "Get the code for a paper, by arXiv ID or title. Returns two different things, and the second is usually the one you want: `repos` -- repositories the authors declared or that were cross-referenced from Papers with Code. Pointers to GitHub; nothing here has been run. `verified_reference_implementations` -- present only when this graph actually HOLDS an implementation of one of the paper's methods. Most are machine-verified: EXECUTED on a synthesized fixture (V1), some with a second generated implementation agreeing on those inputs (V2). There are no held-out tests and no rung compares against the authors' own code. The report sits behind the level (an unbacked level is reported as 0). Fetch these with get_reference_implementation rather than cloning a repo and guessing which file matters. Returns {\"error\": ...} if the paper can't be resolved.",
      "inputSchema": {
        "properties": {
          "id_or_title": {
            "default": null,
            "title": "Id Or Title",
            "type": "string"
          },
          "arxiv_id": {
            "default": null,
            "title": "Arxiv Id",
            "type": "string"
          }
        },
        "type": "object",
        "title": "get_code_for_paperArguments"
      }
    },
    {
      "name": "get_concepts_for_paper",
      "title": "Get Concepts For Paper",
      "description": "Get the concepts a paper discusses (LLM-extracted, each claim carries its own confidence), by arXiv ID or title. Returns {\"error\": ...} if the paper can't be resolved.",
      "inputSchema": {
        "properties": {
          "id_or_title": {
            "default": null,
            "title": "Id Or Title",
            "type": "string"
          },
          "arxiv_id": {
            "default": null,
            "title": "Arxiv Id",
            "type": "string"
          }
        },
        "type": "object",
        "title": "get_concepts_for_paperArguments"
      }
    },
    {
      "name": "get_harvested_code_for_paper",
      "title": "Get Harvested Code For Paper",
      "description": "Which of a paper's own functions RAN. By arXiv ID or title, lists every function or class Syntology harvested from the paper's code, each with: whether it executed in Syntology's sandbox (`status`: ran or unverified, with the verification level; \"ran\" means executed on a synthesized input, not that the paper's results reproduce), the contract verdict and behaviour reading where they exist, and the provenance of THIS paper's copy -- repository, path, role (canonical is the authors' own repository; other roles are third-party implementations) and whether its licence lets the code be shown. When identical code was first harvested from another repository, `first_harvested_from` names it and says what get_code will serve. No code is returned: fetch a body with get_code(code_sha256). Complements get_code_for_paper, which lists the paper's repositories and verified implementations of its methods.",
      "inputSchema": {
        "properties": {
          "id_or_title": {
            "default": null,
            "title": "Id Or Title",
            "type": "string"
          },
          "arxiv_id": {
            "default": null,
            "title": "Arxiv Id",
            "type": "string"
          }
        },
        "type": "object",
        "title": "get_harvested_code_for_paperArguments"
      }
    },
    {
      "name": "get_paper",
      "title": "Get Paper",
      "description": "Look up one paper by arXiv ID or title. Returns its full record -- title, abstract, authors, linked code repos, and any peer reviews (raw, per-reviewer, never averaged) -- from the live graph. If this paper's methods have a reference implementation stored here, the response also carries `verified_reference_implementations`: the method name, its verification level (a level with no report behind it is reported as 0), and the exact call to fetch the code. When that key is present you do not need to search GitHub for an implementation -- fetch it. Accepts the identifier as `id_or_title` or as `arxiv_id` -- the same argument under either name, because this surface calls it both things and a caller should not be punished for having learned the other one. Returns {\"error\": ...} if the paper can't be resolved (not found, or the name matches more than one).",
      "inputSchema": {
        "properties": {
          "id_or_title": {
            "default": null,
            "title": "Id Or Title",
            "type": "string"
          },
          "arxiv_id": {
            "default": null,
            "title": "Arxiv Id",
            "type": "string"
          }
        },
        "type": "object",
        "title": "get_paperArguments"
      }
    },
    {
      "name": "get_papers_by_author",
      "title": "Get Papers By Author",
      "description": "Get papers by a given author name. Exact case-insensitive match wins; otherwise the name must be a WHOLE-TOKEN part of exactly one author (\"Neubig\" -> \"Graham Neubig\", reported as match_kind \"name_part\" with requested_name). A fragment several people share returns near_matches, never one of them. The old rule was CONTAINS-then-most-prolific, and it served one person's papers under another's name: \"ansari\" answered with Hadi Pouransari's, outranking the real Abdul Ansari on publication count. Being forgiving about a name string is not the same as deciding between two people by who published more.",
      "inputSchema": {
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object",
        "title": "get_papers_by_authorArguments"
      }
    },
    {
      "name": "get_papers_by_institution",
      "title": "Get Papers By Institution",
      "description": "Get papers whose authors are affiliated with a given institution (university, lab, company). Exact match wins; otherwise a shorter form resolves only when everything the longer name ADDS is institutional wording -- \"Stanford\" -> \"Stanford University\", \"Oxford\" -> \"University of Oxford\". \"MIT\" does NOT resolve to \"MIT-IBM Watson AI Lab\", because \"IBM\" and \"Watson\" name a different organization; it returns near_matches instead. Institution names are not canonicalized in this corpus, so a real typo in the underlying data still won't match, and a short form may legitimately have no node. A miss returns {\"have\": \"unknown_name\", \"near_matches\": [...]}.",
      "inputSchema": {
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object",
        "title": "get_papers_by_institutionArguments"
      }
    },
    {
      "name": "get_reference_implementation",
      "title": "Get Reference Implementation",
      "description": "USE THIS FIRST when you need to implement a named ML method from a paper -- before searching the web. Returns runnable, self-contained Python that has been executed and cross-checked, not a link to read or a snippet to adapt. Measured against web search on the same tasks: ~3x cheaper and ~2x faster at comparable pass rate, because there is no page to fetch, skim, and transcribe. Call it with the method name (e.g. \"GaLore\", \"IS-MBPG\"). If you do not know whether a method is covered, call list_reference_implementations to browse the catalog first -- a miss there is cheap, and its near_matches will catch a slightly-off name. verification_level: V0 imports; V1 executes against a synthesized fixture and returns a value of the recorded type; V2 two implementations GENERATED from the paper's signature and spec agree numerically (1e-6) on fixture inputs -- neither of the two ever read the authors' own code, so V2 measures how unambiguously the paper specifies the method, not fidelity to the authors' implementation (measured 2026-09-11, commit 75eaa77: cross_checked_by is a second LLM on 2,628 of 2,831 served samples); V3 paper-derived property tests pass on both of those same two generations. No rung on this ladder compares the served code against the authors' implementation, so treat every level as evidence about runnability and internal agreement, not about matching the paper's results. It REFUSES rather than serving below min_level, and the refusal names the highest level that exists -- so lowering min_level is a deliberate, informed choice, not a guess. Code carries llm: provenance; treat as reference rather than audited production code, and cite the attributed paper. Returns {\"error\": ...} only when the method genuinely has no implementation.",
      "inputSchema": {
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          },
          "min_level": {
            "default": 2,
            "title": "Min Level",
            "type": "integer"
          }
        },
        "required": [
          "name"
        ],
        "type": "object",
        "title": "get_reference_implementationArguments"
      }
    },
    {
      "name": "get_reviews_for_paper",
      "title": "Get Reviews For Paper",
      "description": "Get a paper's real peer reviews, by arXiv ID or title -- raw per-reviewer rows (rating, confidence, strengths, weaknesses, questions), never averaged into one score, and never carrying reviewer identity. Empty if this paper was never matched during OpenReview sync, not necessarily that no reviews exist. Returns {\"error\": ...} if the paper can't be resolved.",
      "inputSchema": {
        "properties": {
          "id_or_title": {
            "default": null,
            "title": "Id Or Title",
            "type": "string"
          },
          "arxiv_id": {
            "default": null,
            "title": "Arxiv Id",
            "type": "string"
          }
        },
        "type": "object",
        "title": "get_reviews_for_paperArguments"
      }
    },
    {
      "name": "graph_ask",
      "title": "Graph Ask",
      "description": "Ask a free-text question about AI/ML research -- papers, authors, citations, methods, code implementations. Answers cite the graph edges they're grounded in. Read-only. Rate-limited per trial token (20 queries per 30-minute token, see /developers on syntology.ai). Set code_samples=true to also attach declared code repositories for the papers in the answer (default false). Set strict_provenance=true to drop every LLM-derived claim from the answer, keeping only deterministic (never-inferred) ones -- for a caller that only wants claims traceable to an explicit source, not a model's read of one (default false).",
      "inputSchema": {
        "properties": {
          "q": {
            "title": "Q",
            "type": "string"
          },
          "code_samples": {
            "default": false,
            "title": "Code Samples",
            "type": "boolean"
          },
          "strict_provenance": {
            "default": false,
            "title": "Strict Provenance",
            "type": "boolean"
          }
        },
        "required": [
          "q"
        ],
        "type": "object",
        "title": "graph_askArguments"
      }
    },
    {
      "name": "have",
      "title": "Have",
      "description": "Ask whether Syntology holds an implementation of a method or a paper's methods BEFORE spending anything. Give a method name, an arXiv id, or a paper title. Returns one of: verified_code (machine-verified, with the exact fetch call), unverified_code (stored but not executed on a fixture), no_code (we know the method or paper and hold no implementation -- with the best fallback we do have), or unknown_name (we could not match the name; nearby names are listed, and a non-match is not evidence the method is unknown). No code is returned and this call is free: it does not spend your query budget. no_code asserts absence ONLY for the exact name it resolved and carries code_nearby (same-paper methods and name-sharing methods that DO hold code); unknown_name never asserts absence. absence_asserted says which. Accepts the term as `x` or as `name` -- the same argument under either name. Every other code tool here calls it `name`, and a cold caller that reached for the obvious spelling used to get a pydantic `x Field required`, which reads as a broken tool rather than a renamed argument (SESSION_LEDGER_CODE_LANE.md, step 14). `x` stays canonical.",
      "inputSchema": {
        "properties": {
          "x": {
            "default": null,
            "title": "X",
            "type": "string"
          },
          "name": {
            "default": null,
            "title": "Name",
            "type": "string"
          }
        },
        "type": "object",
        "title": "haveArguments"
      }
    },
    {
      "name": "list_reference_implementations",
      "title": "List Reference Implementations",
      "description": "Browse the verified-code index before fetching anything -- method name, origin paper, publication year, verification level, language, no source attached. START HERE when you do not already know whether a method is covered -- scanning the index is far cheaper than a web search, and a query that matches no entry EXACTLY still returns near_matches rather than an empty result, so one stray token in your query does not read as \"not in the catalog\". The companion to get_reference_implementation, for when you don't already know an exact method name to look up. query does a case-insensitive substring match against method name AND paper title; omit it to browse everything. Also accepted as `topic`. Passing an unrecognised name used to be silently ignored, which returned the WHOLE index and read as a ranked answer to the question asked. verification_level semantics match get_reference_implementation exactly: V0 imports; V1 executes against a synthesized fixture and returns a value of the recorded type; V2 two implementations GENERATED from the paper's signature and spec agree numerically at 1e-6 -- neither of the two read the authors' own code, so V2 measures spec clarity, not fidelity to the authors' implementation (measured 2026-09-11, commit 75eaa77); V3 paper-derived property tests pass on both of those same two generations. No rung compares the served code against the authors' implementation. min_level filters to that floor or above. limit is capped at 200; total_matching tells you if there's more beyond what was returned. ORDER: verification_level descending, then method name A-Z, and the response says so in its own `ordering` field. It is not a ranking by quality, popularity or relevance -- none of those is measured here. Most of the catalog sits at the top verification tier, so a browse with no query is an ALPHABETICAL slice of that tier (the response's `browse_note` says which), not a shortlist. Pass a query if you want the list to be about something.",
      "inputSchema": {
        "properties": {
          "query": {
            "default": null,
            "title": "Query",
            "type": "string"
          },
          "min_level": {
            "default": 0,
            "title": "Min Level",
            "type": "integer"
          },
          "limit": {
            "default": 50,
            "title": "Limit",
            "type": "integer"
          },
          "topic": {
            "default": null,
            "title": "Topic",
            "type": "string"
          }
        },
        "type": "object",
        "title": "list_reference_implementationsArguments"
      }
    }
  ]
}
