---
title: "MCP OAuth vs tool authorization: check every call | Len P. van der Hof"
description: A valid OAuth token gets a client into your MCP server. It does not decide which tool may act on which record. What the spec checks and what you must add.
image: "https://lenvanderhof.com/media/generated/blog-hero-mcp-oauth-is-not-tool-authorization-v2.a202a02bc0a6.wide.webp"
---

[AI Systems](https://lenvanderhof.com/en/blog/category/ai-systems/) Research guide

# MCP OAuth is not tool authorization: a valid token is not a yes

OAuth tells your server the token is genuine and meant for it. Your code decides whether this call may approve this expense.

Len P. van der HofPublished 5 October 20268 min read

Getting to the wall is one check. Opening the second cage is another.

Direct answer

On a remote MCP server, OAuth answers two questions: is this access token genuine and issued for this server, and which scopes (named permissions) does it carry? It does not know which scope each tool needs, or that a user may only approve expenses in their own team. You write that map and check it on every tool call, before the tool changes anything. Local MCP servers that run over stdio skip this OAuth flow and take credentials from their environment.

## Key takeaways

- OAuth tells your MCP server that a token is valid, meant for this server, and which scopes it carries.
- Which scope each tool needs, and which records a caller may touch, is your application's rule to write.
- Invalid, expired, or wrong-server token: 401. Valid token without the required scope: 403 with insufficient_scope.
- Hiding a tool from the list is not blocking it. Check every call, then test a call that must fail and confirm nothing changed.

**OAuth on an MCP server checks the key, not what the key may open.** A valid token tells your server that it is genuine, that it was issued for this server, and which scopes it carries. It does not decide whether this call may approve this expense. That rule is yours to write, and to check on every tool call before the tool changes anything.

The rest of this page shows where the line falls, what the MCP specification requires on each side of it, and how to test the call that should fail.

## First, the words in plain English

- **MCP** (Model Context Protocol) is an open standard that lets an AI app use tools offered by a separate program, the MCP server. The [glossary entry](https://lenvanderhof.com/glossary/mcp/) has the short version.
- **OAuth** is the standard behind the “allow this app to access your account?” screen. Instead of your password, the app receives an **access token**: a temporary pass it sends with every request.
- A **scope** is a named permission written into that token, such as `expenses:read` or `expenses:approve`.
- **Authentication** establishes who is calling. **Authorization** decides what they may do.

Checking a token’s scopes is already authorization. It is just coarse: it knows that a token says `expenses:approve`, not which tool needs that scope or which expenses this person may touch.

## A worked example: one token, three tools

Imagine a hypothetical twelve-person design studio. Its AI assistant connects to an MCP server for expenses with three tools: `list_expenses`, `view_expense`, and `approve_expense`.

Priya, the office manager, signs in through OAuth. The assistant receives a valid token, issued for the expense server, carrying `expenses:read`.

Three different questions now get asked, by three different parts of the system:

QuestionWho answers itWhere the rule comes fromWhich app is asking?The authorization server (the service that issues tokens)The MCP specificationIs this token genuine, unexpired, and issued for this server?Your MCP server, validating the tokenThe MCP specificationMay this call approve this particular expense?Your codeNobody but you

Priya’s token passes the first two. When the assistant calls `approve_expense`, only the third question can stop it. Her token lacks `expenses:approve`, so the server refuses.

Now suppose her manager Tom has `expenses:approve`. He asks the assistant to approve an expense that belongs to another team, or one he filed himself. The token is valid and the scope is present. If your code does not check which team the expense belongs to, and who filed it, nothing else will.

## What does the MCP specification require?

The [MCP authorization specification, revision 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization), makes authorization optional. When a server supports it over HTTP (a server reached over the network), it should follow the specification. A local server that runs over stdio (started as a program on the same machine) should not use this flow. It takes its credentials from its environment instead, so the question becomes which credentials that process receives.

For HTTP servers, the main rules are:

- The client sends the token in the `Authorization` header on every request, and never in the web address.
- The client names the server it wants a token for with a `resource` parameter. The server must check that the token was issued for it, and must not accept or pass on tokens meant for anything else.
- Invalid or expired tokens must get a `401` response.
- A valid token without the scope an operation needs should get a `403` response with `insufficient_scope`, listing every scope that operation needs in one go. The client can then “step up”: ask for a new token that combines its earlier scopes with the new ones. Keeping track of that combination is the client’s job.
- Servers must account for scope hierarchies, where a broader scope implies narrower ones. A plain text match can wrongly reject a token that should pass, so write the hierarchy down and test it.

The specification’s page on tools adds a rule people skip: servers must “implement proper access controls”. It requires the control. It does not design it for you.

The same page allows a server to show each caller only the tools its scopes permit when the client asks for the tool list. That is good design, because the model then never sees a button it cannot press. But hiding a tool is not blocking it. The check that counts runs when the tool is called.

## Identifying the app is not granting a permission

The 2026-07-28 revision says authorization servers and clients should support Client ID Metadata Documents (CIMD). The app’s ID is a web address, and the authorization server fetches a short document at that address that describes the app. The older method, where an app registers itself on the fly (Dynamic Client Registration), is deprecated but kept for backwards compatibility.

CIMD tells the authorization server which app is asking. It does not grant `expenses:approve`, and it does not approve a tool call. Identifying the app, validating the token, and deciding whether this call may approve this expense are three separate checks. Passing the first two does not answer the third.

## Write the permission map

This is the artifact that turns the principle into something you can review. One row per tool, with a default for everything else:

ToolRequired scopeExtra ruleChecked wherelist_expensesexpenses:readOnly expenses in the caller’s teamIn the tool’s codeview_expenseexpenses:readThe expense belongs to the caller’s teamIn the tool’s codeapprove_expenseexpenses:approveSame team; not the caller’s own expense; amount under the approval limitIn the tool’s code, before anything is writtenAny tool not in this tableNoneDenyDefault

*[The Agentic Codebase](https://lenvanderhof.com/books/the-agentic-codebase/)*, one of Len’s books (available now), writes the same idea into a short contract file per tool: a list of what the tool is allowed, a list of what it is denied, and any scope not on the allow list is refused. You do not need the book to use the table above.

Approving money is also a decision many teams keep with a person no matter what the token says. [What an agent may never do](https://lenvanderhof.com/en/blog/override-doctrine/) covers that list.

## Where can the check live?

In the tool’s own code, in shared authorization code that every tool calls, or in a gateway (a proxy that sits in front of the server). The specification does not require a separate policy engine. Pick the place that covers every protected tool and that your team can read and test.

A gateway has one practical help from the specification. Over HTTP, every request must carry `Mcp-Method` and `Mcp-Name` headers, such as `tools/call` and `approve_expense`, so a gateway can see which tool is being called without reading the message body. The body remains the source of truth.

## How do vendors handle it?

These are implementation choices, not requirements of MCP.

**WorkOS** says in its [build notes](https://workos.com/blog/how-to-build-an-mcp-app-on-the-2026-07-28-spec-with-workos-authkit) of 31 July 2026 that its AuthKit product does not ship a scope-per-tool feature, so you create a permission such as `expenses:approve` and check it inside each tool. Their summary is worth pinning up: “‘Access to the server’ isn’t a permission worth having. ‘Can call approve_expense’ is.” One caution: their example helper allows any tool that has no permission mapped. Decide that default on purpose before you copy it.

**Permit.io** takes the gateway route. Its [gateway overview](https://docs.permit.io/permit-mcp-gateway/overview) says that on each call the gateway verifies the token, asks whether this agent may call this tool on this server, and logs the decision. Its co-founder Or Weis [argues](https://www.permit.io/blog/mcp-auth-vs-tool-call-authorization-2026-07-28) that OAuth is necessary but not sufficient. That is a vendor’s view of its own category, and it matches the specification’s split.

## What does a passing connection test leave untested?

A connection test can pass while these defects remain:

- **Every tool accepts the same broad scope.** Looking up an expense and approving one need different access, but both accept the same token.
- **The permission was checked only at sign-in.** Later calls run without checking the scope they need.
- **Record rules are missing.** The tool checks the scope but ignores an input that selects another team’s expense.

[MCP explained for founders](https://lenvanderhof.com/en/blog/mcp-explained-for-founders/) has a wider fifteen-minute review of a server; this page is only the authorization slice.

## Test the call that must fail

Use one valid token for two calls: one it may make and one it must not. For example, allow an expense lookup and refuse an approval when the token lacks `expenses:approve`. Then test the other failure paths one at a time:

TestExpected resultMissing, invalid, or expired token401; no protected tool runsValid token issued for another server401; no protected tool runsValid token without the required scope403 insufficient_scope; the tool does not runRequired scope present, but a record rule forbids the actionYour application’s refusal; nothing changesRequired scope and record rules satisfiedThe action succeeds

A record-rule refusal is not an `insufficient_scope` error. Tom’s token already has the scope. Telling his app to fetch a bigger token cannot fix a rule about which team owns the expense.

Check the record, not just the response. An error message returned after the expense was approved is a failed check.

## Try this today (20 minutes)

1. List every tool on one MCP server you run or depend on.
2. Fill in the permission map above for each one, including the last row: what happens to a tool nobody listed?
3. With a valid token that lacks one permission, call the tool that needs it.
4. Open the record afterwards. If it changed, you found the gap before a customer did.

Cite this:MCP OAuth is not tool authorization: a valid token is not a yes.Len P. van der Hof. [https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/](https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/) · Published 5 October 2026.

## Terminology

- [MCP](https://lenvanderhof.com/glossary/mcp/)

## Sources

1. [Authorization, MCP specification 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) · Model Context Protocol
2. [Tools, MCP specification 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) · Model Context Protocol
3. [Streamable HTTP transport, MCP specification 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) · Model Context Protocol
4. [How to build an MCP app on the 2026-07-28 spec with WorkOS AuthKit](https://workos.com/blog/how-to-build-an-mcp-app-on-the-2026-07-28-spec-with-workos-authkit) · WorkOS
5. [Permit MCP Gateway overview](https://docs.permit.io/permit-mcp-gateway/overview) · Permit.io
6. [MCP Auth vs Tool-Call Authorization After the 2026-07-28 Spec](https://www.permit.io/blog/mcp-auth-vs-tool-call-authorization-2026-07-28) · Permit.io
7. [MCP (glossary)](https://lenvanderhof.com/glossary/mcp/)
8. [MCP explained for founders](https://lenvanderhof.com/en/blog/mcp-explained-for-founders/)
9. [The Agentic Codebase](https://lenvanderhof.com/books/the-agentic-codebase/)

## Further reading

- [MCP explained for founders](https://lenvanderhof.com/en/blog/mcp-explained-for-founders/)
- [MCP meaning](https://lenvanderhof.com/en/blog/mcp-meaning/)
- [What is an MCP connector?](https://lenvanderhof.com/en/blog/what-is-an-mcp-connector/)
- [The Agentic Codebase](https://lenvanderhof.com/books/the-agentic-codebase/)

About the author

## [Len P. van der Hof](https://lenvanderhof.com/en/authors/len-p-van-der-hof/)

Entrepreneur, AI Innovator and Venture Builder

Len P. van der Hof builds practical AI systems, digital ventures and evidence-informed tools for founders.

```json
{
	"@context": "https://schema.org",
	"@graph": [
		{
			"@type": "Person",
			"@id": "https://lenvanderhof.com/#person",
			"name": "Len P. van der Hof",
			"alternateName": [
				"Len van der Hof",
				"L.P. van der Hof",
				"Leendert Pieter van der Hof"
			],
			"honorificSuffix": "MSc",
			"url": "https://lenvanderhof.com/",
			"image": [
				"https://lenvanderhof.com/photos/len-portrait-1.jpg",
				"https://lenvanderhof.com/photos/len-portrait-2.jpg",
				"https://lenvanderhof.com/photos/len-portrait-3.jpg",
				"https://lenvanderhof.com/photos/len-portrait-4.jpg",
				"https://lenvanderhof.com/photos/len-speaking.jpg",
				"https://lenvanderhof.com/photos/len-hero.jpg"
			],
			"jobTitle": "Entrepreneur, AI Innovator and Venture Builder",
			"description": "Len P. van der Hof, MSc, is a Dutch entrepreneur and AI innovator in Zwijndrecht. He builds ReasonKit, MindSesh, Undominated.ai, books under his name, the fiction imprint LPH98.lifestyle, and technology ventures through LPH98.ventures. Eleven titles in Systems for the Strategic Self are available now, in English and Dutch.",
			"address": {
				"@type": "PostalAddress",
				"addressLocality": "Zwijndrecht",
				"addressCountry": "NL"
			},
			"alumniOf": {
				"@type": "CollegeOrUniversity",
				"name": "Rotterdam School of Management, Erasmus University"
			},
			"knowsAbout": [
				"Artificial intelligence",
				"AI agents",
				"Agentic AI systems",
				"LLM routing",
				"SEO",
				"Generative engine optimization",
				"Answer engine optimization",
				"Venture building",
				"Founder performance",
				"Founder psychology",
				"Evidence-based decision-making"
			],
			"sameAs": [
				"https://www.linkedin.com/in/lenvanderhof/",
				"https://x.com/LenvanderHof",
				"https://www.youtube.com/channel/UCTG20buKqYYbitqqf7l3zJA",
				"https://www.instagram.com/Lenvanderhof/",
				"https://www.threads.com/@lenvanderhof",
				"https://github.com/Lenvanderhof",
				"https://huggingface.co/LPH98",
				"https://www.npmjs.com/~lenvanderhof",
				"https://www.goodreads.com/author/show/70983905.Len_P_van_der_Hof",
				"https://www.amazon.com/author/lenvanderhof",
				"https://www.bol.com/nl/nl/b/len-p-van-der-hof-msc/609879394/",
				"https://bsky.app/profile/lenvanderhof.com",
				"https://mastodon.social/@Lenvanderhof",
				"https://crates.io/users/Lenvanderhof",
				"https://cursor.com/@Lenvanderhof",
				"https://medium.com/@Lenvanderhof",
				"https://gitlab.com/Lenvanderhof",
				"https://hub.docker.com/u/lenvanderhof/",
				"https://dev.to/lenvanderhof",
				"https://www.facebook.com/Lenvanderhof",
				"https://soundcloud.com/Lenvanderhof"
			],
			"affiliation": [
				{
					"@id": "https://lenvanderhof.com/#publisher"
				},
				{
					"@id": "https://lenvanderhof.com/#mindsesh"
				},
				{
					"@id": "https://lenvanderhof.com/#lifestyle"
				}
			]
		},
		{
			"@type": "WebSite",
			"@id": "https://lenvanderhof.com/#website",
			"url": "https://lenvanderhof.com/",
			"name": "Len P. van der Hof",
			"description": "Len P. van der Hof, MSc, is a Dutch entrepreneur and AI innovator in Zwijndrecht. He builds ReasonKit, MindSesh, Undominated.ai, books under his name, the fiction imprint LPH98.lifestyle, and technology ventures through LPH98.ventures. Eleven titles in Systems for the Strategic Self are available now, in English and Dutch.",
			"inLanguage": [
				"en",
				"nl"
			],
			"publisher": {
				"@id": "https://lenvanderhof.com/#person"
			}
		},
		{
			"@type": "Organization",
			"@id": "https://lenvanderhof.com/#publisher",
			"name": "LPH98.ventures",
			"url": "https://lph98.ventures",
			"founder": {
				"@id": "https://lenvanderhof.com/#person"
			}
		},
		{
			"@type": "Organization",
			"@id": "https://lenvanderhof.com/#mindsesh",
			"name": "MindSesh",
			"url": "https://mindsesh.net",
			"founder": {
				"@id": "https://lenvanderhof.com/#person"
			}
		},
		{
			"@type": "SoftwareApplication",
			"@id": "https://lenvanderhof.com/#reasonkit",
			"name": "ReasonKit",
			"url": "https://reasonkit.sh",
			"creator": {
				"@id": "https://lenvanderhof.com/#person"
			}
		},
		{
			"@type": "SoftwareApplication",
			"@id": "https://lenvanderhof.com/#undominated",
			"name": "Undominated.ai",
			"url": "https://undominated.ai",
			"creator": {
				"@id": "https://lenvanderhof.com/#person"
			}
		},
		{
			"@type": "Organization",
			"@id": "https://lenvanderhof.com/#lifestyle",
			"name": "LPH98.lifestyle",
			"url": "https://lph98.lifestyle",
			"founder": {
				"@id": "https://lenvanderhof.com/#person"
			}
		},
		{
			"@type": "ImageObject",
			"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#primaryimage",
			"url": "https://lenvanderhof.com/media/generated/blog-hero-mcp-oauth-is-not-tool-authorization-v2.a202a02bc0a6.wide.webp",
			"contentUrl": "https://lenvanderhof.com/media/generated/blog-hero-mcp-oauth-is-not-tool-authorization-v2.a202a02bc0a6.wide.webp",
			"representativeOfPage": true
		},
		{
			"@type": "BreadcrumbList",
			"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#breadcrumb",
			"itemListElement": [
				{
					"@type": "ListItem",
					"position": 1,
					"name": "Home",
					"item": "https://lenvanderhof.com/"
				},
				{
					"@type": "ListItem",
					"position": 2,
					"name": "Blog",
					"item": "https://lenvanderhof.com/en/blog/"
				},
				{
					"@type": "ListItem",
					"position": 3,
					"name": "AI Systems",
					"item": "https://lenvanderhof.com/en/blog/category/ai-systems/"
				},
				{
					"@type": "ListItem",
					"position": 4,
					"name": "MCP OAuth is not tool authorization: a valid token is not a yes",
					"item": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/"
				}
			]
		},
		{
			"@type": "WebPage",
			"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#webpage",
			"url": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/",
			"name": "MCP OAuth is not tool authorization: a valid token is not a yes",
			"description": "A valid OAuth token gets a client into your MCP server. It does not decide which tool may act on which record. What the spec checks and what you must add.",
			"isPartOf": {
				"@id": "https://lenvanderhof.com/#website"
			},
			"primaryImageOfPage": {
				"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#primaryimage"
			},
			"breadcrumb": {
				"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#breadcrumb"
			},
			"inLanguage": "en-GB"
		},
		{
			"@type": "BlogPosting",
			"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#article",
			"mainEntityOfPage": {
				"@id": "https://lenvanderhof.com/en/blog/mcp-oauth-is-not-tool-authorization/#webpage"
			},
			"headline": "MCP OAuth is not tool authorization: a valid token is not a yes",
			"description": "A valid OAuth token gets a client into your MCP server. It does not decide which tool may act on which record. What the spec checks and what you must add.",
			"datePublished": "2026-10-05T14:00:00.000Z",
			"author": {
				"@id": "https://lenvanderhof.com/#person"
			},
			"publisher": {
				"@id": "https://lenvanderhof.com/#person"
			},
			"image": [
				"https://lenvanderhof.com/media/generated/blog-hero-mcp-oauth-is-not-tool-authorization-v2.a202a02bc0a6.square.webp",
				"https://lenvanderhof.com/media/generated/blog-hero-mcp-oauth-is-not-tool-authorization-v2.a202a02bc0a6.landscape.webp",
				"https://lenvanderhof.com/media/generated/blog-hero-mcp-oauth-is-not-tool-authorization-v2.a202a02bc0a6.wide.webp"
			],
			"articleSection": "AI Systems",
			"inLanguage": "en-GB"
		}
	]
}
```
