NAME

Langertha::Raider::MCP::Config - Internal model of the configured MCP servers: sources, precedence, expansion, approvals

VERSION

version 0.504

SYNOPSIS

# Internal to Langertha-Raider -- no API promise.
my $mcp = Langertha::Raider::MCP::Config->from_context( $context, config => $config );

for my $s (@{ $mcp->servers }) {
  printf "%s (%s, %s): %s\n", $s->{name}, $s->{scope}, $s->{source}, $s->{status};
}
my @start = $mcp->startable;            # status 'configured'
my $ms    = $mcp->startup_timeout_ms;   # MCP_TIMEOUT, 30000
my $tool  = $mcp->tool_name($start[0], 'create-issue');   # mcp__github__create-issue

DESCRIPTION

Internal module. Its interface may change without notice.

The MCP servers a person configured, as one value (ADR 0029): where they come from, which entry of a name wins, what ${VAR} expands to, the names their tools get, the timeouts, and whether a project's server may start. It reads files and the environment and starts nothing; connecting is Langertha::Raider::MCP::Server's business.

Sources, highest precedence first (ADR 0029 §2):

1. raider-project -- mcp: of the project config ("mcp_servers" in Langertha::Raider::Config)
2. raider-home -- mcp: of ~/.raider/config.yml
3. local -- projects["PROJECT DIR"].mcpServers of Claude's global config ("claude_config_file"); Claude layout only
4. project -- mcpServers of .mcp.json in "project_dir" and every directory above it, the nearest file first ("mcp_json_files"); Claude layout only
5. user -- the top-level mcpServers of Claude's global config; Claude layout only
6. plugin -- every installed and enabled plugin ("mcp_servers" in Langertha::Raider::Context::Plugins), named plugin:PLUGIN:SERVER; Claude layout only

The first entry of a name wins whole; the others are listed as shadowed. With "bare" there are no servers at all. A file that cannot be read or parsed is reported in "errors", never fatal; a raider config whose mcp: has the wrong shape croaks (from Langertha::Raider::Config).

root

The directory raider runs in: the working directory of stdio servers. Required.

project_dir

The project directory: where .mcp.json is read, the key of the local scope and of the approvals, ${CLAUDE_PROJECT_DIR} of plugin servers. In the Claude layout "project_dir" in Langertha::Raider::Context::Layout, otherwise "root" (the default).

claude_layout

True when Claude Code's sources are read (ADR 0019 §1). Default false.

bare

True for --bare: no server from any source. Default false.

config

The Langertha::Raider::Config whose mcp: and mcp_approvals: are read; undef (the default) reads no raider file.

settings

The Langertha::Raider::Context::Settings of the Claude layout, for the approvals of project servers; undef outside it.

plugins

The Langertha::Raider::Context::Plugins of the Claude layout; undef outside it.

home

The user's home directory, by default "home_dir" in Langertha::Raider::Home.

claude_home

Claude Code's directory, by default "claude_home" in Langertha::Raider::Home of "home".

claude_config_file

Claude Code's global config: .claude.json in $CLAUDE_CONFIG_DIR when that is set and not empty, else ~/.claude.json. undef without either.

env

The variables ${VAR} expands from and MCP_TIMEOUT / MCP_TOOL_TIMEOUT are read from: by default a copy of %ENV taken when first needed (after the Claude settings' env is applied).

session_approvals

The person's answers of this session (ADR 0029 §7), which count before every file: { NAME => 'approved' | 'rejected', '*' => 'approved' } (*: every project server). Default empty. The application keeps one hash for the session and hands it to every model it builds, so an answer holds even where it could not be written down.

bypass_approval

True when the session was started in bypassPermissions (by --permission-mode or --dangerously-skip-permissions). Together with skipDangerousModePermissionPrompt in the user settings ("skip_dangerous_mode_prompt" in Langertha::Raider::Context::Settings) every project server then counts as approved, as in Claude Code; a disabled entry still rejects (ADR 0029 Addendum 1). Default false.

from_context

my $mcp = Langertha::Raider::MCP::Config->from_context( $context, config => $config );

A model for a Langertha::Raider::Context: its root, home, Claude home, layout (the project directory in the Claude layout), settings and plugins; "bare" for mode bare. %more goes to new ("config", "env").

normalize_name

my $part = $mcp->normalize_name('my.server');   # my_server

Every character outside A-Z a-z 0-9 _ - becomes _ (Claude Code's rule), for a server and a tool name alike.

tool_name

my $name = $mcp->tool_name($server, 'create.issue');   # mcp__github__create_issue

The name a tool of $server (an entry of "servers") reaches the model, the permission rules and the hooks with: mcp__KEY__TOOL.

is_credential_variable

$mcp->is_credential_variable('ANTHROPIC_API_KEY');   # 1

True for the variables that are never expanded into the url or headers of a remote server: Claude Code's ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK, and raider's engine keys LANGERTHA_*_API_KEY.

mcp_json_files

for my $f ($mcp->mcp_json_files) { say $f->{label}, ' ', $f->{file} }

The .mcp.json files that exist, nearest first: the one in "project_dir", then those of every directory above it up to the root (ADR 0029 Addendum 1). Each { file, label }; the label is .mcp.json for the project directory, ~/.mcp.json for the home directory, else the absolute path. A name in a nearer file wins over the same name farther up (scope project for all of them).

trust

my $t = $mcp->trust;   # { trusted => 1, dir => '/home/me/dev', source => '~/.claude.json projects["/home/me/dev"]' }

Whether the project directory is trusted (ADR 0029 §7). Raider has no trust decision of its own yet (ADR 0004), so Claude Code's record counts: hasTrustDialogAccepted: true in the projects entry of "project_dir" or of a directory above it in Claude's global config. dir and source name the entry; both undef when untrusted. Only read in the Claude layout (outside it nothing is trusted -- and nothing asks).

approvals

my $a = $mcp->approvals;

The decisions about project servers (ADR 0029 §7), from the Claude settings (Claude layout; filtered by the workspace trust) and the mcp_approvals: entry of "project_dir" in the home config:

{
  enable_all => { source => '.claude/settings.local.json', by => 'enableAllProjectMcpServers' } | undef,
  enabled    => { github => { source => ..., by => 'enabledMcpjsonServers' }, ... },
  disabled   => { db     => { source => ..., by => 'disabledMcpjsonServers' }, ... },
  ignored    => [ { name => 'x', by => 'enabledMcpjsonServers', source => '.claude/settings.json',
                    reason => 'the folder is not trusted' }, ... ],
}

A disabled entry counts from every file, trusted or not. An enabled entry and enableAllProjectMcpServers count from ~/.claude/settings.json always, from .claude/settings.json in a trusted folder, from .claude/settings.local.json in a trusted folder or the configuration home; the others are listed in ignored. enableAllProjectMcpServers is the last counted file's value; the home config's enable_all approves all as well. The first source of a name is kept; "session_approvals" come before every file (source this session, by the person).

approval_target

my $t = $mcp->approval_target;
# { kind => 'claude-settings', file => '/p/.claude/settings.local.json', label => '.claude/settings.local.json' }
# { kind => 'raider-home', file => '/home/me/.raider/config.yml', label => '~/.raider/config.yml', path => '/p' }

Where an interactive approval is written so that it counts next time (ADR 0029 §7): .claude/settings.local.json of "project_dir" in the Claude layout when that file's approvals count (a trusted folder, or the configuration home); otherwise the mcp_approvals: of the home config under the real path of "project_dir". undef when neither is possible (no "config", or no home).

pending

The entries of "servers" waiting for an approval (status pending).

record_approval

my $where = $mcp->record_approval( 'github', 'approve' );   # or 'all', 'reject'

Writes the person's answer about a pending project server where it counts next time ("approval_target"), as Claude Code writes it:

  • .claude/settings.local.json: approve adds the name to enabledMcpjsonServers, all sets enableAllProjectMcpServers to true, reject adds the name to disabledMcpjsonServers. Other keys are kept (the file is written back with sorted keys).

  • the home config: the same under mcp_approvals of the project dir's real path (enabled, enable_all, disabled), through "update_home" in Langertha::Raider::Config.

Also records it in "session_approvals". Returns the label of the file written, or undef when there is nowhere to write (the answer then holds for the session only). Croaks on an unknown answer, or when the settings file exists but is not a JSON object.

startup_timeout_ms

MCP_TIMEOUT of "env": how long one server may take to start (connect, initialize, list its tools), in milliseconds. Default 30000.

tool_timeout_ms

MCP_TOOL_TIMEOUT of "env": the per-call limit in milliseconds for servers without a timeout of their own. undef (no limit) when unset.

max_output_tokens

MAX_MCP_OUTPUT_TOKENS of "env": how many tokens of a tool result raider passes on before it cuts the text (ADR 0029 Addendum 1). Default 25000, Claude Code's.

env_warnings

The problems of those three variables (a value that is not a positive whole number is ignored), as strings.

servers

for my $s (@{ $mcp->servers }) { ... }

Every configured server, the winning entry of each name, in precedence order (plugins in plugin order), each a hash:

{
  name        => 'github',                  # plugin servers: 'plugin:PLUGIN:SERVER'
  key         => 'github',                  # the server part of the tool names
  tool_prefix => 'mcp__github__',
  scope       => 'raider-project' | 'raider-home' | 'local' | 'project' | 'user' | 'plugin',
  source      => '.mcp.json',               # the file (and entry) it came from
  transport   => 'stdio' | 'http' | 'sse' | 'ws' | 'sdk' | undef,
  command     => 'npx', args => [ ... ], env => { ... },   # stdio, expanded
  url         => 'https://...', headers => { ... },        # http, expanded
  cwd         => '/abs/root',               # stdio servers run here
  project_dir => '/abs/project',            # their CLAUDE_PROJECT_DIR
  timeout_ms  => 60000 | undef,             # per call: timeout, else MCP_TOOL_TIMEOUT
  status      => 'configured' | 'pending' | 'rejected' | 'disabled'
               | 'unsupported' | 'not_configured' | 'invalid',
  reason      => '...' | undef,
  warnings    => [ '${DB_URL} is not set', ... ],
  approval    => { required => 0|1, state => 'not required' | 'approved' | 'pending' | 'rejected',
                   source => ..., by => ... },
  shadowed    => [ { scope => 'user', source => '~/.claude.json' }, ... ],
  plugin      => { name, key, root, data_dir } | undef,
  entry       => { ... },                   # as written
}

Only configured servers are started ("startable"). The status is decided in this order: rejected / disabled (the person's decision), invalid, unsupported, not_configured, pending.

errors

Problems of whole files, each { source, error }: Claude's global config or .mcp.json that does not parse, a broken plugin file. Never fatal.

server

my $s = $mcp->server('github');

The entry of "servers" with that name, or undef.

startable

The entries of "servers" with status configured.

expand

my @warnings;
my $text = $mcp->expand('${API:-https://x}/mcp', vars => \%plugin_vars,
  remote => 1, warnings => \@warnings);

${VAR} and ${VAR:-default} in $text (ADR 0029 §4): vars first, then "env"; a set variable (empty included) gives its value, an unset one its default; with neither the text stays as written and ${VAR} is not set is pushed to warnings. With remote (a url or headers value) a credential variable ("is_credential_variable") expands to the empty string, its default ignored, with a warning.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests at https://github.com/Getty/langertha-raider/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

https://raudssus.de/