Skip to content

vault set rejects clientInfo from OAuth dynamic client registration #286

Description

@feniix

What happens

mcporter vault set <server> --stdin rejects client info that came from OAuth dynamic client registration. It prints:

[mcporter] Vault payload clientInfo.redirect_uris must be a string.

redirect_uris is an array, not a string. RFC 7591 registration responses always return it as an array. So any real DCR result is rejected and nothing is saved.

This worked in 0.12.0. It broke in 0.12.1 and is still broken in 0.13.0.

Steps to reproduce

  1. Create ~/.mcporter/mcporter.json:

    {"mcpServers":{"demo":{"url":"https://example.test/mcp","auth":"oauth"}}}
  2. Pipe a payload to vault set:

    echo '{"tokens":{"access_token":"fake","token_type":"Bearer"},"clientInfo":{"client_id":"abc","redirect_uris":["https://example.test/cb"],"grant_types":["authorization_code"],"response_types":["code"],"token_endpoint_auth_method":"none"}}' \
      | mcporter vault set demo --stdin

The access token is fake, so no real credential is involved. The server URL is never contacted.

What you get

On 0.12.1 and later, it exits with code 1 and prints:

[mcporter] Vault payload clientInfo.redirect_uris must be a string.

On 0.12.0, the same command succeeds:

Saved OAuth credentials for 'demo' to <data-dir>/mcporter/credentials.json

If you run the 0.12.0 case, remove the test entry afterwards with mcporter vault clear demo.

What I expected

The payload is saved. redirect_uris is a normal part of client info.

Where it comes from

In dist/cli/vault-command.js:

function validateOAuthClientInfo(clientInfo) {
    for (const [key, value] of Object.entries(clientInfo)) {
        if (value !== undefined && value !== null && typeof value !== 'string') {
            throw new CliUsageError(`Vault payload clientInfo.${key} must be a string.`);
        }
    }
}

This requires every value to be a string. But dist/oauth-client-info.d.ts types client info as OAuthClientInformationMixed from the MCP SDK, and in @modelcontextprotocol/sdk/shared/auth that is:

type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull;

Neither member of that union can pass the check:

  • OAuthClientInformation has client_id_issued_at and client_secret_expires_at, both numbers.
  • OAuthClientInformationFull adds redirect_uris, grant_types and response_types, all arrays of strings.

So the only client info the command accepts is client_id plus optional string fields. Any registration response that carries issuance timestamps or metadata is refused, even though the declared type allows it.

--tokens-file has the same problem. Both sources go through validateVaultPayload, so there is no way around it.

Versions

Version Result
0.12.0 works
0.12.1 fails
0.12.2 fails
0.12.3 fails
0.12.4 fails
0.13.0 fails

Node 24, macOS. Nothing here looks platform specific.

Suggested fix

Check each field for its own type instead of requiring strings everywhere:

  • redirect_uris, grant_types, response_types — array of strings
  • client_id_issued_at, client_secret_expires_at — number
  • client_id, client_secret, client_name, token_endpoint_auth_method — string

Dropping the clientInfo check and keeping the tokens checks would also fix it.

Related

#244 is also about clientInfo on vault entries, but the other way around: it goes missing after authorization. These may be worth looking at together.

Workaround

For anyone blocked on this: the write goes through if you trim clientInfo to the string-only subset the validator accepts before piping it to vault set.

With jq:

jq -c '.clientInfo |= with_entries(select(.value | type == "string"))' payload.json \
  | mcporter vault set demo --stdin

Applied to the payload from the report, that leaves {"client_id":"abc","token_endpoint_auth_method":"none"} and the command succeeds on 0.12.1+.

The same trim in TypeScript, if you are shelling out to vault set from code:

/** The string-only subset of client info that `mcporter vault set` accepts. */
interface VaultClientInfo {
  client_id: string;
  token_endpoint_auth_method: string;
  client_secret?: string;
  client_name?: string;
}

function toVaultClientInfo(info: OAuthClientInformationFull): VaultClientInfo {
  return {
    client_id: info.client_id,
    token_endpoint_auth_method: info.token_endpoint_auth_method,
    ...(info.client_secret !== undefined ? { client_secret: info.client_secret } : {}),
    ...(info.client_name !== undefined ? { client_name: info.client_name } : {}),
  };
}

What it costs

Dropping redirect_uris, grant_types, response_types, client_id_issued_at and client_secret_expires_at is only safe when the caller owns registration:

  • Refreshes keep working. A refresh_token grant sends no redirect URI, and the MCP SDK reads client_id and token_endpoint_auth_method when it authenticates the refresh call — so those two are the ones worth keeping.
  • If you depend on mcporter re-registering the client, the trimmed entry no longer carries the metadata for it. We do registration ourselves and hand mcporter the result, so mcporter never re-registers.

This is a workaround, not a fix. The validator still contradicts the OAuthClientInformationMixed type the command declares it accepts, and a schema-aware validator would let the full registration response through.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Urgent regression or broken agent/channel workflow affecting real users now.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions