Noodle Seed
Guides

Machine-to-machine MCP authentication

Let scheduled jobs, backend agents, and CI automation call a deployed MCP server with a least-privilege service principal.

OAuth Client Credentials is currently Preview. Existing deployments and interactive OAuth clients continue to work as before. Adding machine access requires no app-code change and no redeploy.

Suppose a reporting worker runs every morning and calls a read-only generate_report tool. No person is present to open a browser or approve an OAuth consent screen. Give that workload its own organization-owned service principal instead of sharing a person's login.

A service principal belongs to your Noodle Seed organization. Each grant authorizes it for one app and one environment with an explicit OAuth scope ceiling. Its credential is exchanged for a short-lived, resource-bound access token that works only at the exact deployed MCP URL.

When to use machine authentication

Use a service principal when software calls MCP without a person present:

ScenarioRecommended authentication
A scheduled reporting or synchronization jobService principal
A backend agent responding to queue eventsService principal
CI/CD calling a validation or deployment toolService principal
A server-to-server integrationService principal
ChatGPT or Claude with a person presentInteractive OAuth, not a service principal
A customer using your product through an MCP clientCustomer auth
Noodle calling a downstream business APIA separately managed connector credential

Create a separate principal for each workload. That makes its permissions, activity, rotation, and revocation independent from every other automation.

How the flow works

Loading diagram…

The service-principal credential proves which workload is calling. The grant limits where it may call and which scopes it may request. The tool's own authorization remains in force.

An inbound MCP bearer token is never forwarded to a business backend. If the tool calls another API, Noodle resolves that API's connector credential separately through the credential broker.

Before you start

You need:

  • An existing deployed app and environment. This guide uses acme / reporting / prod.
  • The exact deployed MCP URL, such as https://acme.cloud.noodleseed.dev/reporting/mcp.
  • A read-only tool to verify. This guide uses generate_report with the scope reports.read.
  • Organization-owner access to manage service principals.
  • Node.js 24 or newer for the example worker.
  • A secrets manager, workload identity store, or equivalent protected location for the private credential.

If the tool declares requiredScopes, use those exact scope tokens. A service principal has no human roles, so a tool that also requires an allowed role is not eligible for machine access.

1. Create the service principal

Open the Noodle Console, choose the organization, then open Organization settings and Machine access.

Select Create service principal, enter Reporting worker, and create it. Open the new OAuth service principal and copy its principal ID. The principal ID is also the OAuth client_id your worker will use.

The identity belongs to the organization rather than one app. It cannot call anything until you create an app/environment grant and an active credential.

2. Grant access to one deployment target

In Grants, select:

FieldExample value
Appreporting
Environmentprod
OAuth scope tokensreports.read

Select Create grant. Noodle does not infer wildcard scopes. If you leave the scope list empty, the principal can reach only otherwise-eligible tools that do not declare requiredScopes.

A principal can have only one active grant for the same app and environment. To change that grant, revoke it and create its replacement. That produces a deliberate interruption, so plan scope changes separately from credential rotation.

3. Add a credential

The Credentials section offers two choices:

MethodUse
Public keyRecommended. The worker signs a short-lived private_key_jwt; Noodle stores only the public JWK.
Client secretFallback for automation platforms that support only client_secret_basic. The secret is displayed once.

Generate the key inside your secrets-management workflow when possible. For a local Node.js 24 setup, save this as generate-machine-key.mjs and run it from a private working directory:

import { generateKeyPairSync, randomUUID } from 'node:crypto'
import { writeFileSync } from 'node:fs'

const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
const kid = randomUUID()

const publicJwk = {
  ...publicKey.export({ format: 'jwk' }),
  alg: 'RS256',
  use: 'sig',
  key_ops: ['verify'],
  kid,
}
const privateJwk = {
  ...privateKey.export({ format: 'jwk' }),
  alg: 'RS256',
  use: 'sig',
  key_ops: ['sign'],
  kid,
}

writeFileSync('machine-auth-public.jwk', `${JSON.stringify(publicJwk, null, 2)}\n`, {
  mode: 0o644,
})
writeFileSync('machine-auth-private.jwk', `${JSON.stringify(privateJwk, null, 2)}\n`, {
  mode: 0o600,
})

In the Console, enter the label primary, optionally set an expiry, choose machine-auth-public.jwk, and select Create public key. The Console accepts RS256/RSA and ES256/P-256 public JWKs. It rejects private or symmetric key material.

Upload machine-auth-private.jwk to the worker's secrets manager. Confirm that the stored secret is usable, then remove the local private copy. Never upload the private file to Noodle or commit either generated file to source control.

Fallback: create a client secret

Choose Client secret, use a label such as primary, optionally set an expiry, and select Create client secret. Copy the client ID and secret directly into your secrets manager. The Console shows the secret once and cannot retrieve it later.

Client secrets are long-lived credentials. Prefer a public key when your automation platform can sign JWT assertions.

CLI equivalent

You can provision the same identity from the CLI. Create the principal first and copy principalId from the JSON response:

noodle auth service-principals create "Reporting worker" --org acme --json

Then create the exact grant and add only the public key:

noodle auth service-principals grant <principal-id> \
  --org acme \
  --app reporting \
  --env prod \
  --scope reports.read

noodle auth service-principals add-jwk <principal-id> \
  --org acme \
  --label primary \
  --file ./machine-auth-public.jwk

If public-key authentication is unavailable, create a fallback secret. Its value is returned once:

noodle auth service-principals create-secret <principal-id> \
  --org acme \
  --label fallback

See the noodle auth reference for expiry options, JSON output, and the complete service-principal command set.

4. Call MCP from the worker

Install the official MCP v2 client in the worker project:

pnpm add @modelcontextprotocol/client@2

Save the following as reporting-worker.mjs. It loads configuration at runtime, negotiates the supported MCP era automatically, obtains an access token, and calls the read-only tool:

import { readFile } from 'node:fs/promises'
import {
  Client,
  PrivateKeyJwtProvider,
  StreamableHTTPClientTransport,
} from '@modelcontextprotocol/client'

const mcpUrl = required('MCP_URL')
const issuer = required('MCP_ISSUER')
const clientId = required('MCP_CLIENT_ID')
const privateKey = JSON.parse(await readFile(required('MCP_PRIVATE_JWK_FILE'), 'utf8'))

const client = new Client(
  { name: 'reporting-worker', version: '1.0.0' },
  { capabilities: {}, versionNegotiation: { mode: 'auto' } },
)

await client.connect(
  new StreamableHTTPClientTransport(new URL(mcpUrl), {
    authProvider: new PrivateKeyJwtProvider({
      clientId,
      privateKey,
      algorithm: 'RS256',
      expectedIssuer: issuer,
      scope: 'reports.read',
    }),
  }),
)

try {
  const result = await client.callTool({
    name: 'generate_report',
    arguments: { period: 'previous_day' },
  })
  if (result.isError) throw new Error('generate_report returned an error')
  console.log('Report generated successfully')
} finally {
  await client.close()
}

function required(name) {
  const value = process.env[name]
  if (!value) throw new Error(`${name} is required`)
  return value
}

Configure the worker through its deployment platform rather than a checked-in .env file:

MCP_URL=https://acme.cloud.noodleseed.dev/reporting/mcp
MCP_ISSUER=https://cloud.noodleseed.dev
MCP_CLIENT_ID=<principal-id>
MCP_PRIVATE_JWK_FILE=/run/secrets/machine-auth-private.jwk

Run the worker with node reporting-worker.mjs. Replace the example tool name, arguments, and scope with the read-only tool from your deployment.

What the SDK sends

The auth provider discovers the Noodle authorization server, then sends a token request containing:

  • grant_type=client_credentials
  • resource=https://acme.cloud.noodleseed.dev/reporting/mcp
  • scope=reports.read
  • A short-lived private_key_jwt assertion signed by the registered private key

Noodle verifies the credential and exact grant, then returns a bearer token with a ten-minute lifetime and no refresh token. The SDK obtains another token when needed. The token is accepted only for the exact MCP resource URL, and the gateway rechecks live revocation before dispatching a call.

Use a client secret when required

If the workload platform cannot sign JWT assertions, replace the auth provider with ClientCredentialsProvider. Read the one-time secret from your secrets manager at runtime:

import { ClientCredentialsProvider } from '@modelcontextprotocol/client'

const authProvider = new ClientCredentialsProvider({
  clientId: required('MCP_CLIENT_ID'),
  clientSecret: required('MCP_CLIENT_SECRET'),
  expectedIssuer: required('MCP_ISSUER'),
  scope: 'reports.read',
})

Pass authProvider to StreamableHTTPClientTransport as in the complete example. This uses client_secret_basic; it does not place the secret in the request body. Never hard-code or print the secret.

5. Verify before enabling the schedule

Use the repository smoke against an explicitly read-only tool before letting a scheduler or event consumer run unattended:

pnpm smoke:mcp-client-credentials -- \
  --url https://acme.cloud.noodleseed.dev/reporting/mcp \
  --issuer https://cloud.noodleseed.dev \
  --client-id <principal-id> \
  --private-jwk-file ./machine-auth-private.jwk \
  --scope reports.read \
  --tool generate_report \
  --args-json '{"period":"previous_day"}'

The private JWK file must have 0600 permissions. For the fallback secret path, put the secret in a named environment variable and replace --private-jwk-file with --client-secret-env MCP_CLIENT_SECRET. The smoke does not accept a secret value on the command line and does not print credentials, access tokens, tool arguments, or tool output.

Rotate credentials without downtime

Credentials can overlap. Rotate one without interrupting the worker:

  1. Add a new public key or client secret with a distinct label.
  2. Update the worker's secrets manager and restart or reload the worker.
  3. Run the read-only smoke with the new credential.
  4. Revoke the old credential only after the new path succeeds.

For CLI-managed public keys, add the replacement with noodle auth service-principals add-jwk, then revoke the old credential:

noodle auth service-principals revoke-credential <principal-id> <credential-id> \
  --org acme \
  --yes

Use the Console's Replace action for the same overlap-first sequence. The old credential remains active until you explicitly confirm revocation.

Revoke access

Choose the narrowest action that matches the incident or lifecycle event:

ActionEffect
Revoke one credentialStops that key or secret while other credentials and the grant remain active.
Revoke one grantStops this principal from calling that app/environment.
Revoke the service principalStops all of the principal's grants and credentials. This cannot be undone.
noodle auth service-principals revoke-credential <principal-id> <credential-id> \
  --org acme \
  --yes

noodle auth service-principals revoke-grant <principal-id> <grant-id> \
  --org acme \
  --yes

noodle auth service-principals revoke <principal-id> \
  --org acme \
  --yes

Revocation blocks future token exchanges and already-issued machine tokens. Existing lifecycle metadata remains visible in Recent lifecycle so organization members can understand what changed.

Troubleshooting

SymptomWhat to check
invalid_clientThe principal and credential are active, the correct private key or secret is loaded, the assertion uses RS256 or ES256, and the issuer is exact. This error is deliberately generic.
invalid_targetMCP_URL is the exact currently served URL and the principal has an active grant for its app and environment. Do not add a query, fragment, or alternate hostname.
invalid_scopeEvery requested scope is unique and falls within the active grant's scope ceiling.
The tool is not advertisedCheck the tool name, required scopes, read-only annotation for the smoke, and any role requirement.
A role-gated tool is unavailableService principals have scopes but no human roles. Use a machine-eligible tool or change the tool's deliberate authorization design.
A previously working token is deniedCheck Recent lifecycle for principal, grant, or credential revocation and verify which app/environment the worker targets.

Authentication errors do not reveal whether a principal or credential record exists. Use the Console or noodle auth service-principals show <principal-id> --org acme while signed in as an organization member to inspect safe lifecycle metadata.

Security checklist

  • Use one principal per workload rather than sharing one identity across unrelated jobs.
  • Grant one exact app/environment and only the scopes the worker needs.
  • Prefer a public key and a short-lived private_key_jwt assertion over a shared client secret.
  • Keep private keys and secrets in a secrets manager; never commit them or put them in command arguments.
  • Set expectedIssuer and use the exact resource URL to prevent credential and token misrouting.
  • Cache access tokens only until their expiry and obtain a new token instead of expecting a refresh token.
  • Add and verify a replacement credential before revoking the old one.
  • Never log credentials, assertions, access tokens, request headers, or token responses.
  • Never forward an MCP bearer token to a connector or business backend.
  • Revoke a credential, grant, or principal promptly when its workload is retired or compromised.

The flow follows the official MCP OAuth Client Credentials extension, while Noodle Seed adds organization ownership, exact deployment grants, live revocation, and tenant-safe operational evidence around it.

On this page