# `openalex__openalex_search_entities`

**Openalex Search Entities**

Search, filter, sort, or retrieve by ID. Covers all OpenAlex entity types (works, authors, sources, institutions, topics, keywords, publishers, funders). Pass \`id\` to retrieve a single entity. Otherwise, use \`query\` and/or \`filters\` for discovery. Supports keyword search with boolean operators, exact phrase matching, and AI semantic search. Use openalex_resolve_name to resolve names to IDs before filtering. Searches and ID lookups return a curated set of fields by default; pass \`select\` to override with specific fields, or \`\["*"\]\` for the full record.

- Server: [OpenAlex](https://gateway.mcpharbor.dev/docs/servers/openalex) (`dev.mcpharbor.gateway/openalex`)
- Price per call: $0.0001 (100 micro-USD); only tools/call is charged
- Endpoint: `https://gateway.mcpharbor.dev/mcp` or `https://gateway.mcpharbor.dev/mcp/openalex`
- Upstream tool: exposed by the provider; called through this gateway as `openalex__openalex_search_entities`
- Provider terms: <https://openalex.org/OpenAlex_termsofservice.pdf> (verified 2026-09-19)
- Upstream API documentation: <https://help.openalex.org/>
- Rate limit: 600 requests per minute
- Attribution required: Appreciated but not required (CC0)

## Parameters

- `cursor` (string, optional): Pagination cursor from a previous response. Pass to get the next page.
- `entity_type` (string, required): Type of scholarly entity to search. One of: `works`, `authors`, `sources`, `institutions`, `topics`, `keywords`, `publishers`, `funders`.
- `filters` (object, optional): Filter criteria as field:value pairs. AND across fields (multiple keys). OR within field: pipe-separate ("us\|gb"). NOT: prefix "!" ("!us"). Range: "2020-2024". Comparison: "\>100", "\<50". AND withi...
- `id` (string, optional): Retrieve a single entity by ID. Supports: OpenAlex ID ("W2741809807"), DOI ("10.1038/nature12373"), ORCID ("0000-0002-1825-0097"), ROR ("https://ror.org/00hx57361"), PMID ("12345678" or "https://pu...
- `per_page` (integer, optional): Results per page (1-100). Default 25. Semantic search caps at 50 — when search_mode="semantic", set per_page ≤ 50 (also subject to a 1 req/sec rate limit upstream). The cap applies to searches only... Default: `25`.
- `query` (string, optional): Text search query. Supports boolean operators (AND, OR, NOT), quoted phrases ("exact match"), wildcards (machin*), fuzzy matching (machin~1), and proximity ("climate change"~5). Omit for filter-onl...
- `sample` (integer, optional): Return a random sample of this many entities matching the filters (1-100). Single page only — pagination via \`cursor\` is not supported with sampling. Overrides \`per_page\`. Useful for unbiased explo...
- `search_mode` (string, optional): Search strategy. "keyword": stemmed full-text (default). "exact": no stemming, matches individual words (use quoted phrases for multi-word exact match). "semantic": AI embedding similarity (max 50 ... One of: `keyword`, `exact`, `semantic`. Default: `keyword`.
- `seed` (string, optional): Deterministic seed for \`sample\`. Same seed + same filters = same results — pass when reproducibility matters. Has no effect without \`sample\`, and a search that passes it alone is rejected.
- `select` (array, optional): OpenAlex top-level field names to return. Always returned: \`id\`, \`display_name\` — additional fields you list are appended. A curated default per entity type applies to both searches and single-enti...
- `sort` (string, optional): Sort field. Prefix with "-" for descending. Comma-separate for a multi-key sort, applied left to right, with the "-" prefix set per key ("-publication_year,cited_by_count" sorts by year descending,...

## Example call

Request headers and body:

```http
POST /mcp HTTP/1.1
Host: gateway.mcpharbor.dev
Authorization: Bearer <your gateway API key>
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: openalex__openalex_search_entities
```

```json
{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": {
        "name": "example-agent",
        "version": "1.0.0"
      },
      "io.modelcontextprotocol/protocolVersion": "2026-07-28"
    },
    "arguments": {
      "entity_type": "works"
    },
    "name": "openalex__openalex_search_entities"
  }
}
```

A successful response:

```json
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "_meta": {
      "dev.mcpharbor.gateway/call": {
        "chargedMicroUsd": 100,
        "id": "<the id of this charge>"
      }
    },
    "content": [
      {
        "text": "<whatever the upstream tool returned>",
        "type": "text"
      }
    ],
    "resultType": "complete"
  }
}
```

This call costs $0.0001 (100 micro-USD). Reading this page and listing the tool cost nothing.
The `dev.mcpharbor.gateway/call` record in the result is the receipt for that charge. Each
POST is metered separately, so sending the same call again buys another one: see
[agent.txt](https://gateway.mcpharbor.dev/agent.txt), section 7, before retrying.

## Attribution

This provider requires the attribution below wherever you use results from its tools.
Reproduce it verbatim:

> Appreciated but not required (CC0)

## Tool definition

Exactly as `tools/list` returns it (free):

```json
{
  "_meta": {
    "dev.mcpharbor.gateway/pricing": {
      "currency": "USD",
      "pricePerCallMicroUsd": 100
    }
  },
  "annotations": {
    "idempotentHint": true,
    "openWorldHint": true,
    "readOnlyHint": true
  },
  "description": "Search, filter, sort, or retrieve by ID. Covers all OpenAlex entity types (works, authors, sources, institutions, topics, keywords, publishers, funders). Pass `id` to retrieve a single entity. Otherwise, use `query` and/or `filters` for discovery. Supports keyword search with boolean operators, exact phrase matching, and AI semantic search. Use openalex_resolve_name to resolve names to IDs before filtering. Searches and ID lookups return a curated set of fields by default; pass `select` to override with specific fields, or `[\"*\"]` for the full record.",
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "additionalProperties": false,
    "properties": {
      "cursor": {
        "description": "Pagination cursor from a previous response. Pass to get the next page.",
        "type": "string"
      },
      "entity_type": {
        "description": "Type of scholarly entity to search.",
        "enum": [
          "works",
          "authors",
          "sources",
          "institutions",
          "topics",
          "keywords",
          "publishers",
          "funders"
        ],
        "type": "string"
      },
      "filters": {
        "additionalProperties": {
          "type": "string"
        },
        "description": "Filter criteria as field:value pairs. AND across fields (multiple keys). OR within field: pipe-separate (\"us|gb\"). NOT: prefix \"!\" (\"!us\"). Range: \"2020-2024\". Comparison: \">100\", \"<50\". AND within same field: \"+\"-separate. Use OpenAlex IDs (not names) for entity filters — resolve names first. Common keys: `openalex` (filter by entity ID, e.g. {\"openalex\": \"W123|W456\"}), `cites` (works citing a given work), `publication_year` (range \"2020-2024\"), `authorships.author.id`, `type`, `is_oa`.",
        "propertyNames": {
          "type": "string"
        },
        "type": "object"
      },
      "id": {
        "description": "Retrieve a single entity by ID. Supports: OpenAlex ID (\"W2741809807\"), DOI (\"10.1038/nature12373\"), ORCID (\"0000-0002-1825-0097\"), ROR (\"https://ror.org/00hx57361\"), PMID (\"12345678\" or \"https://pubmed.ncbi.nlm.nih.gov/12345678\"), ISSN (\"1234-5678\"). A PMCID is recognized too, bare (\"PMC1234567\") or as a PubMed Central URL, but OpenAlex indexes no PMCIDs, so it resolves nothing — pass the work's PMID or DOI instead. When provided, `query`, `search_mode`, `filters`, `sort`, `sample`, and `seed` are not applied — the returned record is the entity at that ID regardless of them, and the response `notice` names any you passed. `select` still applies: the curated per-entity-type default is returned unless you pass `select` (use `[\"*\"]` for the complete record). To filter, drop `id` and search. Use openalex_resolve_name to find the ID if unknown.",
        "type": "string"
      },
      "per_page": {
        "default": 25,
        "description": "Results per page (1-100). Default 25. Semantic search caps at 50 — when search_mode=\"semantic\", set per_page ≤ 50 (also subject to a 1 req/sec rate limit upstream). The cap applies to searches only; an `id` lookup returns its one record regardless of both.",
        "maximum": 100,
        "minimum": 1,
        "type": "integer"
      },
      "query": {
        "description": "Text search query. Supports boolean operators (AND, OR, NOT), quoted phrases (\"exact match\"), wildcards (machin*), fuzzy matching (machin~1), and proximity (\"climate change\"~5). Omit for filter-only queries.",
        "type": "string"
      },
      "sample": {
        "description": "Return a random sample of this many entities matching the filters (1-100). Single page only — pagination via `cursor` is not supported with sampling. Overrides `per_page`. Useful for unbiased exploration: spot-checking filter correctness, stratified review prompts, or generating exploration sets without bias toward most-cited.",
        "maximum": 100,
        "minimum": 1,
        "type": "integer"
      },
      "search_mode": {
        "default": "keyword",
        "description": "Search strategy. \"keyword\": stemmed full-text (default). \"exact\": no stemming, matches individual words (use quoted phrases for multi-word exact match). \"semantic\": AI embedding similarity (max 50 results, 1 req/sec).",
        "enum": [
          "keyword",
          "exact",
          "semantic"
        ],
        "type": "string"
      },
      "seed": {
        "description": "Deterministic seed for `sample`. Same seed + same filters = same results — pass when reproducibility matters. Has no effect without `sample`, and a search that passes it alone is rejected.",
        "type": "string"
      },
      "select": {
        "description": "OpenAlex top-level field names to return. Always returned: `id`, `display_name` — additional fields you list are appended. A curated default per entity type applies to both searches and single-entity (`id`) lookups; pass field names to override it, or `[\"*\"]` to retrieve the complete record (every field). Only top-level fields project, so a nested value is requested by its parent object: bibliometrics (`h_index`, `i10_index`, `2yr_mean_citedness`) live under `summary_stats` on authors, sources, institutions, publishers, and funders, and naming a leaf returns that object. Invalid field names produce an error identifying the rejected field. Example: [\"doi\", \"authorships\", \"primary_topic\"].",
        "items": {
          "type": "string"
        },
        "type": "array"
      },
      "sort": {
        "description": "Sort field. Prefix with \"-\" for descending. Comma-separate for a multi-key sort, applied left to right, with the \"-\" prefix set per key (\"-publication_year,cited_by_count\" sorts by year descending, then citations ascending). Common: \"cited_by_count\", \"-publication_date\", \"-relevance_score\" (default when query present). Note: when combined with a keyword query, an explicit sort overrides relevance ranking entirely — top results may be highly cited but only tangentially on-topic. Use \"-relevance_score\" or omit sort to keep the most relevant results first. \"-relevance_score\" requires an active search via \"query\" or a \"filter:search\" filter — passing it without one will fail.",
        "type": "string"
      }
    },
    "required": [
      "entity_type"
    ],
    "type": "object"
  },
  "name": "openalex__openalex_search_entities",
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "additionalProperties": false,
    "anyOf": [
      {
        "not": {
          "required": [
            "error"
          ]
        },
        "required": [
          "meta",
          "results",
          "echo",
          "totalCount"
        ]
      },
      {
        "required": [
          "error"
        ]
      }
    ],
    "properties": {
      "budget": {
        "additionalProperties": false,
        "description": "What this call cost against the OpenAlex daily budget and what is left of it. Price a full traversal before committing to it: `totalCount` ÷ `per_page` × `costUsd` against `remainingUsd`. Absent when OpenAlex omitted the accounting headers.",
        "properties": {
          "costUsd": {
            "description": "USD this call spent. 0 for an `id` lookup — OpenAlex does not bill single-entity fetches, so batching known IDs beats paging a filtered list.",
            "type": "number"
          },
          "prepaidRemainingUsd": {
            "description": "USD left in the prepaid balance — a separate pool OpenAlex draws on only after the daily allowance runs out, and which the daily reset does not refill. Add it to `remainingUsd` for the full spendable amount. Absent when the account holds no prepaid balance.",
            "type": "number"
          },
          "remainingUsd": {
            "description": "USD left in today's OpenAlex budget after this call.",
            "type": "number"
          },
          "resetsInSeconds": {
            "description": "Seconds until the daily budget refills (midnight UTC).",
            "type": "number"
          }
        },
        "required": [
          "costUsd",
          "remainingUsd",
          "resetsInSeconds"
        ],
        "type": "object"
      },
      "echo": {
        "description": "Compact echo of the criteria that actually ran (entity_type, query, filters, sort, search_mode) — surfaces what was searched when results are empty. An `id` lookup echoes entity_type and id alone, because the search criteria are not applied on that path.",
        "type": "string"
      },
      "error": {
        "additionalProperties": {},
        "description": "Present when the call failed. Absent on success.",
        "properties": {
          "code": {
            "description": "JSON-RPC error code for this failure.",
            "maximum": 9007199254740991,
            "minimum": -9007199254740991,
            "type": "integer"
          },
          "data": {
            "additionalProperties": {},
            "properties": {
              "reason": {
                "description": "Machine-readable failure mode. Declared by this tool: `semantic_per_page_cap`: A search (no `id`) set per_page above the semantic-search cap of 50. `sample_with_cursor`: A search (no `id`) provided both `sample` and `cursor`. `seed_without_sample`: A search (no `id`) provided `seed` without `sample`. `entity_not_found`: Lookup by id matched no OpenAlex entity. `rate_limited`: OpenAlex throttled the request for exceeding its per-second ceiling (HTTP 429). `upstream_budget_exhausted`: The OpenAlex daily usage budget is spent (HTTP 429). `upstream_timeout`: OpenAlex did not respond within the request deadline. `upstream_unavailable`: OpenAlex was unreachable or unusable — HTTP 503, a connection failure, or a body that was empty, HTML, or unparseable JSON. `upstream_unauthorized`: OpenAlex rejected the API key (HTTP 401). `upstream_forbidden`: OpenAlex denied access to the requested resource (HTTP 403). `comma_in_filter_value`: A filter value contains a comma, which collides with the OpenAlex filter separator. `upstream_invalid_params`: OpenAlex rejected an invalid filter, select, or sort field name (HTTP 400). `upstream_invalid_id_value`: An entity-ID filter received a value that is not an OpenAlex ID — usually a name (HTTP 400). `upstream_sort_requires_search`: sort=-relevance_score was used without an active search (HTTP 400). `upstream_invalid_params_other`: OpenAlex rejected the request (HTTP 400) for a reason other than an invalid field name. `upstream_validation_failed`: OpenAlex rejected the request as semantically invalid (HTTP 422). Other values are possible when a failure originates below the handler.",
                "examples": [
                  "semantic_per_page_cap",
                  "sample_with_cursor",
                  "seed_without_sample",
                  "entity_not_found",
                  "rate_limited",
                  "upstream_budget_exhausted",
                  "upstream_timeout",
                  "upstream_unavailable",
                  "upstream_unauthorized",
                  "upstream_forbidden",
                  "comma_in_filter_value",
                  "upstream_invalid_params",
                  "upstream_invalid_id_value",
                  "upstream_sort_requires_search",
                  "upstream_invalid_params_other",
                  "upstream_validation_failed"
                ],
                "type": "string"
              },
              "recovery": {
                "additionalProperties": {},
                "description": "Actionable next step for the caller.",
                "properties": {
                  "hint": {
                    "type": "string"
                  }
                },
                "required": [
                  "hint"
                ],
                "type": "object"
              },
              "retryable": {
                "description": "Whether retrying may succeed.",
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "message": {
            "description": "Human-readable description of what went wrong.",
            "type": "string"
          }
        },
        "required": [
          "code",
          "message"
        ],
        "type": "object"
      },
      "meta": {
        "additionalProperties": false,
        "description": "Result metadata including pagination.",
        "properties": {
          "count": {
            "description": "Total results matching the query/filters.",
            "type": "number"
          },
          "next_cursor": {
            "description": "Cursor for next page. null if no more results.",
            "type": [
              "string",
              "null"
            ]
          },
          "per_page": {
            "description": "Results on this page.",
            "type": "number"
          }
        },
        "required": [
          "count",
          "per_page",
          "next_cursor"
        ],
        "type": "object"
      },
      "notice": {
        "description": "Guidance notice. Set when results are empty (echoes the criteria and suggests how to broaden) or when an `id` lookup was passed search criteria it does not apply (names them). Absent otherwise.",
        "type": "string"
      },
      "results": {
        "description": "OpenAlex entity objects passed through unchanged. Additional fields depend on entity_type and select.",
        "items": {
          "additionalProperties": {},
          "description": "A single OpenAlex entity record. `id` is always present and `display_name` is always returned (though it may be null); additional fields vary by entity_type and `select`.",
          "properties": {
            "display_name": {
              "description": "Entity name or work title. null when OpenAlex holds no title for the record (paratext works and other untitled entries) — use `id` to identify it.",
              "type": [
                "string",
                "null"
              ]
            },
            "id": {
              "description": "OpenAlex ID (e.g., \"W2741809807\", \"A1234567890\").",
              "type": "string"
            }
          },
          "required": [
            "id",
            "display_name"
          ],
          "type": "object"
        },
        "type": "array"
      },
      "totalCount": {
        "description": "Total results matching the query/filters across all pages.",
        "type": "number"
      }
    },
    "type": "object"
  },
  "title": "Openalex Search Entities"
}
```
