feat(mcp): add idx-mcp, a stdio MCP server over idx-core

Phase 4 of docs/DESIGN.md. Eleven grouped tools (quotes, history, screen,
company, analysis, compare, shareholders, investor holdings, search,
ownership changes, status); ownership_sync is listed only with
--allow-sync. Live results keep the Fetched envelope, errors use the CLI's
{error, code, message} body, and investor tools on an empty database
point at the sync path.

idx-core gains Period/Interval FromStr, Investor::status, a ticker
filter on changes, and Display for ErrorCode (now used by the CLI too).
CLI output and goldens are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 18:19:04 +00:00
commit cda12fe2db
18 changed files with 1546 additions and 38 deletions

View file

@ -294,6 +294,17 @@ pub enum Period {
}
impl Period {
pub const ALL: [Self; 8] = [
Self::OneDay,
Self::FiveDays,
Self::OneMonth,
Self::ThreeMonths,
Self::SixMonths,
Self::OneYear,
Self::TwoYears,
Self::FiveYears,
];
pub fn as_str(&self) -> &'static str {
match self {
Self::OneDay => "1d",
@ -320,6 +331,8 @@ pub enum Interval {
}
impl Interval {
pub const ALL: [Self; 3] = [Self::Day, Self::Week, Self::Month];
pub fn as_str(&self) -> &'static str {
match self {
Self::Day => "1d",
@ -328,3 +341,37 @@ impl Interval {
}
}
}
impl std::str::FromStr for Period {
type Err = crate::error::IdxError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
parse_named(value, "period", &Self::ALL, Self::as_str)
}
}
impl std::str::FromStr for Interval {
type Err = crate::error::IdxError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
parse_named(value, "interval", &Self::ALL, Self::as_str)
}
}
fn parse_named<T: Clone>(
value: &str,
what: &str,
all: &[T],
name: fn(&T) -> &'static str,
) -> Result<T, crate::error::IdxError> {
all.iter()
.find(|item| name(item) == value)
.cloned()
.ok_or_else(|| {
let names: Vec<_> = all.iter().map(name).collect();
crate::error::IdxError::InvalidInput(format!(
"invalid {what} '{value}' (expected one of: {})",
names.join(", ")
))
})
}