dotnet odata-mcp) is an MCP server on your machine for one OData HTTP API. It reads $metadata, builds a compact catalog, and speaks MCP to your harness over stdio. This page is the technical companion to Use Local MCP.
The service root
Every command takes the service root, for examplehttps://host/odata. Local MCP appends $metadata itself. A pasted URL that ends in /$metadata (with or without ?$format=xml) is trimmed to its root; $metadata anywhere else in the path is left alone. The root always ends in / internally so relative segments append instead of replacing the last path segment.
Startup sequence (start)
- Build the host. One generic host owns everything: the authenticating
"OData"HTTP client, the plain"OAuth"client used to talk to authorization servers, the token cache, the CSDL parser, the catalog options, and (forstart) the stdio MCP server with itsshutdown_servertool. There is exactly oneBuild(), and nothing touches the network before it, because the two named clients must share oneIHttpClientFactoryfor the negotiated credential to reach the tools. - Fetch and parse
$metadatathrough the"OData"client. If the service answers 401, the authentication handler runs discovery, acquires a token (interactively or not, depending on flags), and retries. A restart with a still-valid cached token spends exactly one request here. - Build the catalog.
ODataMcpCatalogturns the parsed model into resources and tools (below). Type shapes are computed once and cached. - Probe the data anonymously. After the catalog exists, Local MCP runs steps 2 to 4 of the service probe through the default, unauthenticated client: one
GET {firstSet}?$top=1and a check of the row against the model. The verdict is logged at Information level. When the data answered 401 or 403 and no credential flag was passed, the model’sinitialize.instructionsgain a one-sentence preface saying that data requires sign-in and that the first call may pause for authentication. Probe failures never block startup, and the interactive sign-in still happens lazily on the first real tool call, so an unattended process does not stall on a prompt at boot. - Serve stdio. JSON-RPC in on stdin, out on stdout. All logging goes to stderr. There is no HTTP transport in Local MCP.
What the harness is given
Instructions.initialize.instructions carries the handful of rules a model cannot infer from schemas: parameter names omit $, prefer named tools, where bound and unbound operations are listed, how to pass arguments to odata_call, and not to read the raw $metadata resource to explore. A developer-supplied preface can precede it.
Generic tools, always present and named odata_*: list entity sets, describe a type, describe the whole model (summary, complete, or a mermaid relationship diagram), query, get by key, create, update, delete, navigate a relationship, list unbound operations, and call an operation. describe_type returns a compact declaration grammar by default (Name?: type, -> Type[], // key, store-generated, enum(A|B), bound operations inline) or a compact JSON form on request. A TripPin Person, with its two enums and four bound operations, is 194 tokens in that grammar against 726 for the equivalent CSDL XML; the whole TripPin service is 346 tokens complete, or 91 as a summary, against 1,859 for the XML. Across the reference services the shapes run roughly 70% smaller than the raw metadata at the same level of detail.
Named tools, one family per entity set while the configured cap allows a complete family: list_{set}, get_{type}, create_{type}, update_{type}, delete_{type}. Create and update schemas are typed from the model: required lists exactly the properties required on create, enums carry their members, maxLength is emitted when short, and every key argument describes how to write the literal.
Validation before HTTP. Create, update, and odata_call are checked against the model first: required-on-create, unknown properties on closed types, JSON kind per EDM type, enum membership, MaxLength, and operation binding (instance, collection, unbound). A bad request fails locally with the declared signature in the message.
shutdown_server. Registered in start only. Lets an assistant end a local development session cleanly.
The probe and try
Microsoft.OData.Mcp.Core.Diagnostics.ServiceProbe answers four questions with three requests and never throws for a misbehaving service:
Verdicts, in precedence order:
MetadataSecured, Unreachable (metadata 404/5xx/transport), Unreadable (metadata not CSDL), DataSecured, Unreachable (data 404/5xx), Unreadable (data not JSON or row does not match), Ready.
dotnet odata-mcp try runs the probe anonymously and prints the four lines plus the verdict. Then:
- Verdict
Ready,Unreachable, orUnreadable: exit 0 or 1 accordingly. - Secured and sign-in flags were passed (
--auth-token, an API key, Basic credentials,--client-id, or--grant): probe again through the"OData"client, which signs in exactly asstartwould, and print the second report. Exit 0 if that pass isReady. - Secured and no flags: run
OAuthDiscoveryfrom the captured challenge and print the protected resource metadata status, the authorization server issuer, the advertised grants, and the scope from the challenge, followed by the command to run next. No grant is started. Exit 2.
Transport
Standard MCP stdio to the harness. One JSON-RPC message per line on stdout; nothing else may be written there. Logging isMicrosoft.Extensions.Logging console output routed entirely to stderr, at Information by default and Debug with --verbose. MCP SDK internals are filtered to Warning unless --verbose is set.
Outbound authentication
Local MCP signs in to the OData HTTP API on the operator’s behalf. Happy-path flags: Sign in to the service. Discovery. On a 401 the handler reads theWWW-Authenticate challenge, then looks for RFC 9728 protected resource metadata (the resource_metadata URL from the challenge first, then the well-known locations under the service root), then RFC 8414 or OpenID Connect authorization server metadata at the issuers the resource metadata names. --auth-server supplies the issuer when a service returns only a bare Bearer challenge and publishes nothing. --resource overrides the audience.
Grants. device_code (default for interactive stdio use; the URL and code are printed to stderr), authorization_code with PKCE on a loopback redirect, client_credentials, and identity_assertion through an enterprise IdP. The grant must be advertised by the authorization server. Dynamic client registration is used when the server supports it and no --client-id was given; --client-metadata-document supplies a client metadata document URI for that.
Tokens. Access and refresh tokens live in the operating system credential store through Latchkey, keyed by service root and client id, or in a file-based backend under --token-cache. Refresh happens on the send path before a request goes out; an invalid_token answer to an attached token triggers one forced refresh. Tokens never appear in tool results, logs, or the registration command that add prints.
Mid-session re-authentication. When a token cannot be refreshed while serving stdio, the handler starts the grant, Local MCP elicits the sign-in URL to the harness so it can relay it to the user, completes the grant, and retries the tool call once.
Unattended processes. There is no TTY detection. A daemon must pass --grant client_credentials (and --client-secret or ODATA_MCP_CLIENT_SECRET), or it will fall into an interactive grant nobody is present to complete.
Flags
Shared bystart and try.
Making the OData API self-describing
If you own the OData HTTP API, publish protected resource metadata on it. Local MCP then needs no sign-in flags beyond a client id./.well-known/oauth-protected-resource for the application root, anonymously, and adds resource_metadata="…" to the WWW-Authenticate header on a 401. Set options.Prefixes to any Endpoint Routing or Minimal API route base (including "") when the resource is not the whole host.
Exit codes
Supported OData surface
Entity sets, entity and complex types, enums (including flags), navigation properties, bound and unbound functions and actions,$filter, $select, $orderby, $expand, $top, $skip, $count, key access including composite keys, and create, update (PATCH), and delete. $batch is not used. OData v4 only; a v2 or v3 service shows up in try as unreadable at the results step.