mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
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:
parent
5b8a26499d
commit
cda12fe2db
18 changed files with 1546 additions and 38 deletions
|
|
@ -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(", ")
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ pub enum ErrorCode {
|
|||
NoMarketData,
|
||||
}
|
||||
|
||||
/// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`.
|
||||
impl std::fmt::Display for ErrorCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{self:?}").to_uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl IdxError {
|
||||
pub fn code(&self) -> ErrorCode {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::fs;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::Utc;
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
|
@ -55,6 +55,18 @@ impl Investor {
|
|||
db::query_releases(&self.connect()?)
|
||||
}
|
||||
|
||||
/// Whether the database has data, and which releases it holds.
|
||||
pub fn status(&self) -> Result<OwnershipStatus, IdxError> {
|
||||
let db_path = self.db_path()?;
|
||||
let releases = db::query_releases(&db::open_db_at(&db_path)?)?;
|
||||
Ok(OwnershipStatus {
|
||||
db_path: db_path.display().to_string(),
|
||||
populated: !releases.is_empty(),
|
||||
latest_as_of_date: releases.iter().map(|r| r.as_of_date).max(),
|
||||
releases,
|
||||
})
|
||||
}
|
||||
|
||||
/// Holders of `symbol` in the latest release, optionally limited to one
|
||||
/// source (ranks and concentration are recomputed for that source).
|
||||
pub fn ticker(&self, symbol: &str, source: HolderSource) -> Result<TickerOwnership, IdxError> {
|
||||
|
|
@ -132,8 +144,19 @@ impl Investor {
|
|||
db::query_bing_flow(&conn, ticker_id)
|
||||
}
|
||||
|
||||
pub fn changes(&self, from: &str, to: &str) -> Result<Vec<ChangeRow>, IdxError> {
|
||||
db::query_changes(&self.connect()?, from, to)
|
||||
/// Holder changes between two release dates, optionally for one ticker.
|
||||
pub fn changes(
|
||||
&self,
|
||||
from: &str,
|
||||
to: &str,
|
||||
ticker: Option<&str>,
|
||||
) -> Result<Vec<ChangeRow>, IdxError> {
|
||||
let mut rows = db::query_changes(&self.connect()?, from, to)?;
|
||||
if let Some(ticker) = ticker {
|
||||
let ticker = ticker.trim().to_uppercase();
|
||||
rows.retain(|row| row.ticker_code == ticker);
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn graph(
|
||||
|
|
@ -380,6 +403,15 @@ pub struct MergeResult {
|
|||
pub bing_holdings_updates: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct OwnershipStatus {
|
||||
pub db_path: String,
|
||||
/// False until `sync` or `import` has loaded at least one release.
|
||||
pub populated: bool,
|
||||
pub latest_as_of_date: Option<NaiveDate>,
|
||||
pub releases: Vec<OwnershipRelease>,
|
||||
}
|
||||
|
||||
/// Holder sources for `Investor::ticker`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HolderSource {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue