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"].mcpServersof Claude's global config ("claude_config_file"); Claude layout only - 4.
project--mcpServersof .mcp.json in "project_dir" and every directory above it, the nearest file first ("mcp_json_files"); Claude layout only - 5.
user-- the top-levelmcpServersof Claude's global config; Claude layout only - 6.
plugin-- every installed and enabled plugin ("mcp_servers" in Langertha::Raider::Context::Plugins), namedplugin: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:
approveadds the name toenabledMcpjsonServers,allsetsenableAllProjectMcpServersto true,rejectadds the name todisabledMcpjsonServers. Other keys are kept (the file is written back with sorted keys).the home config: the same under
mcp_approvalsof 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.