Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
EXCELMCP_CLIENT_IDNoAzure AD application client IDbuilt in
EXCELMCP_TENANT_IDNoTenant. Use 'common' for personal accounts.common
EXCELMCP_DEFAULT_FOLDERNoFolder to use when a tool call omits folder_path. The wizard writes this into your agent config.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": true
}
logging
{}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
extensions
{
  "io.modelcontextprotocol/ui": {}
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
get_workspace_graphA

Returns the cached file structure — all filenames, sheet names, column headers, and cross-sheet relationships (inferred at scan time from matching column names plus overlapping sampled values, merged with any the user declared in ~/.excelmcp/relationships.yaml; each carries a confidence score and its evidence). INSTANT — makes no API call. Reads from local graph.json. ALWAYS call this first at session start to orient yourself. Shows you exactly which files exist, what sheets they have, and what columns are in each sheet. The structure varies for every company — never assume, always discover. Also returns sheet_name_variants: groups of sheet names that differ only in case or whitespace across files — check it before any cross-file operation, because those match by exact sheet name. Each sheet carries a regions list: the table bodies found in it, derived from the sheet's own SUM/COUNT formulas, in absolute sheet rows. A sheet with more than one region holds several separate tables (also listed in multi_region_sheets), so a plain aggregate over it adds up blocks that were never meant to be summed — read its unclaimed_rows and check which region you mean before totalling anything. layout_confidence is "unconfirmed" wherever the region map came from formulas alone and nothing has verified it. Use this before any filter_sheet call when unsure which file or column to query.

scan_workspaceA

Rescans the OneDrive folder and rebuilds the structure index and embeddings. SLOW — makes many API calls. ONLY call when: new .xlsx files have been added to OneDrive, or existing sheet names or column headers have changed. DO NOT call this at session start. DO NOT call this before every query. The workspace is already indexed from setup. Use get_workspace_graph for instant structure access.

queryA

Natural language question with automatic RAG routing. Embeds your question, retrieves candidate sheets by vector similarity, reranks them by lexical overlap with column names and sampled values, fetches the winners LIVE from OneDrive, and returns results. Use for exploratory questions when you do not know which specific file or sheet contains the answer. n_results controls how many sheets are fetched (default 5); min_score drops weak matches. CHECK data.routing: when routing_ambiguous is true the top candidates scored within a tie margin and the choice between them is effectively arbitrary — confirm with inspect_file or ask the user instead of trusting one. Including a distinctive literal value in the question (a client name, a material) strongly improves routing. Response metadata.fetched_at confirms this is live data. For known file/sheet combinations use filter_sheet instead.

filter_sheetA

Fetches a specific sheet LIVE from OneDrive and returns rows matching the given conditions. Always live — no cache. Use when you already know which file and sheet to query. Get column names from get_workspace_graph first.

Condition formats (string form): Exact match: {"ColumnName": "value"} Contains: {"ColumnName": "~value"} (literal, not regex) Comparisons: {"ColumnName": ">100"} (also >=, <, <=) Date bounds: {"Batch Date": ">=2026-01-01"} (ISO-8601)

Condition formats (object form, combinable): IN list: {"Status": {"in": ["Closed", "Shipped"]}} Range: {"Qty": {"between": [10, 500]}} Date range: {"Batch Date": {">=": "2026-01-01", "<": "2026-04-01"}} Null check: {"Notes": {"is_null": false}} Contains: {"Name": {"contains": "oxide"}}

Multiple conditions are ANDed together; multiple operators inside one object are ANDed too. An unknown column name or operator is an error, not an empty result.

MATCHING IS NORMALISED, NOT STRICT: exact string matches ignore case and surrounding whitespace ("closed" matches "Closed "), because Excel cells carry stray whitespace constantly. Pass exact_case=true for byte-for-byte matching. Contains (~) is case-insensitive. If zero rows match, the response includes zero_match_diagnostics showing what each condition matched on its own and the values actually present in the column — use it to correct a near-miss and retry instead of concluding the data does not exist. At most 1000 rows are returned; check the truncated and total_matched fields in the response.

aggregateA

Fetches a sheet LIVE and runs a grouped aggregation. Operations: sum, count, mean, min, max. group_by is one column name or a list of them. conditions filters rows before aggregating (same grammar as filter_sheet, including the object form). having filters the AGGREGATED rows afterwards, e.g. having={"Revenue": ">1000"} keeps only groups whose aggregate exceeds 1000. SINGLE FILE ONLY. For totals across multiple files you MUST use cross_file_aggregate instead — never use this tool and then manually add results across files. Get column names from get_workspace_graph first. Returns rows plus a truncated flag. If conditions matched zero rows, zero_match_diagnostics shows what each condition matched alone and the values actually present — correct the condition and retry.

cross_file_aggregateA

MANDATORY for any total spanning more than one file. Fetches relevant sheets from ALL files in PARALLEL, applies filter conditions, returns the aggregate total.

WHEN YOU MUST CALL THIS:

  • Any total, sum, count, or average across multiple files

  • Any cross-file comparison or consolidation

  • Verifying a total you calculated from individual files

NEVER calculate cross-file totals by:

  • Adding individual filter_sheet results in your head

  • Using Python to sum numbers from separate tool calls

  • Guessing based on partial data

Always call this AND show per-file breakdown so the user can verify both agree. If they differ, flag it.

ONLY files whose sheet is named EXACTLY sheet are included in the total. Files without that exact sheet are listed in unmatched_files, with their actual sheet names and did_you_mean candidates — they are NEVER silently included. If the response has a warning, skipped_files, or unmatched_files, surface that to the user: the total may be incomplete. Check sheet_name_variants in get_workspace_graph first to see naming fragmentation before aggregating.

inspect_fileA

Returns structural metadata for one specific file — sheet names, column headers, and each sheet's approx_row_count AS OF THE LAST SCAN (this tool makes no API call, so the count is not live; treat it as an order-of-magnitude hint, not a current figure). INSTANT — reads from cached graph.json. Use before filter_sheet when you need to confirm the exact column names available in a specific file.

join_sheetsA

Joins two sheets LIVE on key columns and returns the merged rows, with filter_sheet's truncation contract (total_matched, truncated, limit). Omit left_on/right_on to let the server pick keys from the workspace's known relationships — it uses a declared or high-confidence inferred relationship and REFUSES with the candidate list when confidence is low, rather than guessing. The keys actually used and their source are in data.keys. Key matching is normalised (case, whitespace, 45 vs 45.0); null keys never join. join_type: inner, left, right, outer. Colliding column names get _left/_right suffixes. Use this instead of stitching filter_sheet results together yourself.

deriveA

Computes a NET value over transaction types in one call: sum of sign * groupwise_sum(quantity_col) across the given components. This is how a stock figure like receipts + purchases − consumption − returns becomes ONE call with the arithmetic done in pandas, instead of five filter_sheet calls added up in your head (which RULE 3 forbids).

components is a list of {"conditions": {...same grammar as filter_sheet...}, "sign": 1 or -1, "label": "receipts"} conditions (optional) pre-filters the sheet before any component applies. The response includes a per-component breakdown with rows_matched. A component that matched ZERO rows is flagged and warned about — check the spelling of the transaction type before trusting the net.

get_cellA

Reads EXACTLY ONE cell, LIVE, by address. One Graph request, tiny payload, no ambiguity. Use when the location is already known — follow-up questions, scheduled routines, anything where lookup or filter_sheet already established the address earlier. address is A1 notation ("B7") or the name of a workbook-scoped named range that resolves to one cell. Serial dates arrive converted to ISO-8601; check resolved_type. A multi-cell address is an error — use filter_sheet for ranges.

lookupA

ONE-CALL semantic lookup: finds a single cell value anywhere in the workspace and returns it WITH PROVENANCE (file, sheet, cell address, the matched row) and a confidence signal. Reads only the key column and the matched row — never whole sheets.

Two ways to call it:

  1. Natural language: lookup(query="contracted rate for Titanium Dioxide under the BESTEX contract"). The server resolves the key value against values sampled at scan time and picks the return column lexically. Works best when the query contains a literal value that appears in the data (a client, a material).

  2. Explicit: lookup(key_column="Material", key_value= "Titanium Dioxide", return_column="Contracted Rate"). Use this when the query form reports it could not parse, or for values too rare to be sampled. scope={"file": ..., "sheet": ...} narrows the search.

READ confidence BEFORE using the value: "high" — single row matched; corroborating sheets (if any) agree. provenance.corroborated_by lists them. "ambiguous" — the key matched SEVERAL ROWS. value is null; every row is in alternatives. Never pick one silently. "conflict" — several sheets DISAGREE. value is null; every version is in alternatives. Surface the conflict to the user. found=false — key not found; suggestions holds fuzzy near-misses (retry with exact spelling), or ambiguity explains why routing failed. NEVER present a value from this tool without citing provenance.file, provenance.sheet and provenance.cell.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Karunya-Muddana/ExcelMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server