Metadata-Version: 2.4
Name: crosstabs
Version: 1.5.0
Summary: MCP package with 39 statistical tools, 6 market-research tools, local database and survey-platform pull tools and runtime status plus 37 headless workflow tools
Project-URL: Homepage, https://crosstabs.com
Project-URL: Documentation, https://crosstabs.com
Project-URL: Source, https://pypi.org/project/crosstabs/#files
Project-URL: Support, https://crosstabs.com/support
Author-email: "crosstabs.com" <support@crosstabs.com>
License-Expression: MIT
License-File: LICENSE
Keywords: chi-square,contingency-tables,cramers-v,crosstabs,fisher-exact,mcp,model-context-protocol,odds-ratio,statistical-analysis,statistics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Requires-Dist: mcp<2,>=1.29
Requires-Dist: numpy>=1.24.0
Requires-Dist: pandas>=2.0.0
Requires-Dist: scipy>=1.10.0
Requires-Dist: statsmodels>=0.14.0
Provides-Extra: all-db
Requires-Dist: cryptography>=42; extra == 'all-db'
Requires-Dist: databricks-sdk>=0.30; extra == 'all-db'
Requires-Dist: databricks-sql-connector>=4.0; extra == 'all-db'
Requires-Dist: databricks-sqlalchemy>=2.0; extra == 'all-db'
Requires-Dist: duckdb-engine>=0.13; extra == 'all-db'
Requires-Dist: duckdb>=1.0; extra == 'all-db'
Requires-Dist: openpyxl>=3.1; extra == 'all-db'
Requires-Dist: oracledb>=2.0; extra == 'all-db'
Requires-Dist: psycopg[binary]>=3.1; extra == 'all-db'
Requires-Dist: pyarrow>=14.0; extra == 'all-db'
Requires-Dist: pymysql>=1.1; extra == 'all-db'
Requires-Dist: pyodbc>=5.0; extra == 'all-db'
Requires-Dist: pyreadstat>=1.2.7; extra == 'all-db'
Requires-Dist: pytz; extra == 'all-db'
Requires-Dist: pyyaml>=6.0; extra == 'all-db'
Requires-Dist: snowflake-sqlalchemy>=1.6; extra == 'all-db'
Requires-Dist: sqlalchemy-bigquery>=1.11; extra == 'all-db'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'all-db'
Requires-Dist: sqlglot>=25.0; extra == 'all-db'
Provides-Extra: bigquery
Requires-Dist: openpyxl>=3.1; extra == 'bigquery'
Requires-Dist: pyarrow>=14.0; extra == 'bigquery'
Requires-Dist: pyreadstat>=1.2.7; extra == 'bigquery'
Requires-Dist: pyyaml>=6.0; extra == 'bigquery'
Requires-Dist: sqlalchemy-bigquery>=1.11; extra == 'bigquery'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'bigquery'
Requires-Dist: sqlglot>=25.0; extra == 'bigquery'
Provides-Extra: databricks
Requires-Dist: databricks-sdk>=0.30; extra == 'databricks'
Requires-Dist: databricks-sql-connector>=4.0; extra == 'databricks'
Requires-Dist: databricks-sqlalchemy>=2.0; extra == 'databricks'
Requires-Dist: openpyxl>=3.1; extra == 'databricks'
Requires-Dist: pyarrow>=14.0; extra == 'databricks'
Requires-Dist: pyreadstat>=1.2.7; extra == 'databricks'
Requires-Dist: pyyaml>=6.0; extra == 'databricks'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'databricks'
Requires-Dist: sqlglot>=25.0; extra == 'databricks'
Provides-Extra: db
Requires-Dist: openpyxl>=3.1; extra == 'db'
Requires-Dist: pyarrow>=14.0; extra == 'db'
Requires-Dist: pyreadstat>=1.2.7; extra == 'db'
Requires-Dist: pyyaml>=6.0; extra == 'db'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'db'
Requires-Dist: sqlglot>=25.0; extra == 'db'
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == 'dev'
Requires-Dist: pip-audit>=2.9.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: tomli>=2.0.0; (python_version < '3.11') and extra == 'dev'
Provides-Extra: duckdb
Requires-Dist: duckdb-engine>=0.13; extra == 'duckdb'
Requires-Dist: duckdb>=1.0; extra == 'duckdb'
Requires-Dist: openpyxl>=3.1; extra == 'duckdb'
Requires-Dist: pyarrow>=14.0; extra == 'duckdb'
Requires-Dist: pyreadstat>=1.2.7; extra == 'duckdb'
Requires-Dist: pytz; extra == 'duckdb'
Requires-Dist: pyyaml>=6.0; extra == 'duckdb'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'duckdb'
Requires-Dist: sqlglot>=25.0; extra == 'duckdb'
Provides-Extra: mssql
Requires-Dist: openpyxl>=3.1; extra == 'mssql'
Requires-Dist: pyarrow>=14.0; extra == 'mssql'
Requires-Dist: pyodbc>=5.0; extra == 'mssql'
Requires-Dist: pyreadstat>=1.2.7; extra == 'mssql'
Requires-Dist: pyyaml>=6.0; extra == 'mssql'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'mssql'
Requires-Dist: sqlglot>=25.0; extra == 'mssql'
Provides-Extra: mysql
Requires-Dist: openpyxl>=3.1; extra == 'mysql'
Requires-Dist: pyarrow>=14.0; extra == 'mysql'
Requires-Dist: pymysql>=1.1; extra == 'mysql'
Requires-Dist: pyreadstat>=1.2.7; extra == 'mysql'
Requires-Dist: pyyaml>=6.0; extra == 'mysql'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'mysql'
Requires-Dist: sqlglot>=25.0; extra == 'mysql'
Provides-Extra: oracle
Requires-Dist: openpyxl>=3.1; extra == 'oracle'
Requires-Dist: oracledb>=2.0; extra == 'oracle'
Requires-Dist: pyarrow>=14.0; extra == 'oracle'
Requires-Dist: pyreadstat>=1.2.7; extra == 'oracle'
Requires-Dist: pyyaml>=6.0; extra == 'oracle'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'oracle'
Requires-Dist: sqlglot>=25.0; extra == 'oracle'
Provides-Extra: postgres
Requires-Dist: openpyxl>=3.1; extra == 'postgres'
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
Requires-Dist: pyarrow>=14.0; extra == 'postgres'
Requires-Dist: pyreadstat>=1.2.7; extra == 'postgres'
Requires-Dist: pyyaml>=6.0; extra == 'postgres'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'postgres'
Requires-Dist: sqlglot>=25.0; extra == 'postgres'
Provides-Extra: redshift
Requires-Dist: openpyxl>=3.1; extra == 'redshift'
Requires-Dist: psycopg[binary]>=3.1; extra == 'redshift'
Requires-Dist: pyarrow>=14.0; extra == 'redshift'
Requires-Dist: pyreadstat>=1.2.7; extra == 'redshift'
Requires-Dist: pyyaml>=6.0; extra == 'redshift'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'redshift'
Requires-Dist: sqlglot>=25.0; extra == 'redshift'
Provides-Extra: snowflake
Requires-Dist: cryptography>=42; extra == 'snowflake'
Requires-Dist: openpyxl>=3.1; extra == 'snowflake'
Requires-Dist: pyarrow>=14.0; extra == 'snowflake'
Requires-Dist: pyreadstat>=1.2.7; extra == 'snowflake'
Requires-Dist: pyyaml>=6.0; extra == 'snowflake'
Requires-Dist: snowflake-sqlalchemy>=1.6; extra == 'snowflake'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'snowflake'
Requires-Dist: sqlglot>=25.0; extra == 'snowflake'
Provides-Extra: surveys
Requires-Dist: google-auth-oauthlib>=1.2; extra == 'surveys'
Requires-Dist: openpyxl>=3.1; extra == 'surveys'
Requires-Dist: pyarrow>=14.0; extra == 'surveys'
Requires-Dist: pyreadstat>=1.2.7; extra == 'surveys'
Description-Content-Type: text/markdown

# Crosstabs MCP Server

<!-- mcp-name: io.github.crosstabs/crosstabs -->

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![MCP](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io)

This 1.5.0 distribution provides two local MCP servers: **39 statistical tools**, **6 market-research tools**, **1 local database pull tool**, **1 survey-platform pull tool** and one runtime-status tool (48 registered tools), and **37 headless research workflow tools** including runtime status, portable analysis plans, guided weighting and Statistics procedures. Both servers report their executing package version through `get_runtime_status`. Individual statistical tools report whether their inference is exact, asymptotic, or simulated. Publication evidence for earlier package versions does not establish this distribution's publication or certification status. Check the executing runtime rather than inferring its version from an installed plugin manifest.

New in 1.5.0 (see "Changes in 1.5.0" below): six market-research tools and a local read-only database pull (`crosstabs pull` and `pull_database_query`) and a survey-platform pull for 14 platforms (`crosstabs pull <platform>` and `pull_survey_responses`) on the statistical server, and two headless Statistics procedure tools, `list_statistical_procedures` and `run_statistical_procedure`, covering 110 SPSS-equivalent procedures.

Guided weighting: supply complete trusted population margins to `preview_raking_weights`, review its aggregate balance/trim/ESS diagnostics, then explicitly call `apply_raking_weights` with the reviewed fingerprint, warning acknowledgements, human approval, expected revision and idempotency key. No respondent weight vector is returned. Apply creates a new saved sampling-weight column, preserving audit/replay/undo; it grants no survey-design or ordinary-inference approval.

Both commands are local stdio servers. The package contains no HTTP/SSE
transport or cloud-deployment configuration; network transport arguments fail
closed. The only outbound connection the package makes is the one you ask for
with `crosstabs pull` / `pull_database_query` / `pull_survey_responses`: from
your machine to your own database or survey-platform account. Use the separate five-tool public MCP at crosstabs.com only for bounded
aggregate matrices and public evidence.

`crosstabs` is an aggregate calculator. Its ordinary inferential tools require caller-attested `inputSemantics="integer_frequency_counts"` and non-negative integer frequency cells; they do not accept respondent, calibration, raking, or complex-survey weights. Use `crosstabs-headless` for a completed supported saved survey design and its disclosed design-aware inference.

Inferential aggregate calls also require `weighted=false`. The two descriptive
aggregate visual tools, `stacked_bar_data` and `correspondence_analysis`, require
`inputSemantics="descriptive_aggregate_values"`; their fractional values are not
survey-weighted inference. `mosaic_plot_data` remains an inferential residual
diagnostic and therefore requires integer frequency counts. Missing or unsupported
semantics, fractional cells, non-finite values, negative values, and unsafe integers
are rejected before any statistics are returned. `proportion_ci` separately requires
`countSemantics="integer_binomial_counts"` and safe integer binomial counts for
`successes` and `total`.

## Features

### Core Statistical Tests
| Test | Description |
|------|-------------|
| **Chi-square** | Pearson's chi-square test of independence |
| **G-test** | Likelihood-ratio alternative with an asymptotic p-value |
| **Fisher's exact** | Two-sided fixed-margin exact p-value for 2×2 integer counts |
| **McNemar's** | Exact two-sided binomial inference for fewer than 20 discordant pairs; continuity-corrected chi-square otherwise |

### Effect Sizes & Measures
| Measure | Use Case |
|---------|----------|
| **Cramér's V** | Effect size for any table size (with bias correction) |
| **Phi coefficient** | Effect size for 2×2 tables |
| **Odds ratio** | Association strength with a large-sample Woolf log interval |
| **Relative risk** | Risk comparison between groups |
| **Risk difference** | Absolute risk reduction |
| **Attributable risk** | Population-level impact |

### Ordinal Measures
| Measure | Description |
|---------|-------------|
| **Spearman's rho** | Rank correlation |
| **Kendall's tau** | Concordance measure |
| **Goodman-Kruskal gamma** | Ordinal association |
| **Somers' D** | Asymmetric ordinal measure |
| **Stuart's tau-c** | Rectangular table measure |

### Agreement & Reliability
| Measure | Description |
|---------|-------------|
| **Cohen's kappa** | Inter-rater agreement with an asymptotic normal interval |
| **Weighted kappa** | Linear/quadratic agreement with an asymptotic normal interval |

### Advanced Analysis
| Tool | Description |
|------|-------------|
| **CMH test** | Stratified analysis with a Robins-Breslow-Greenland pooled-OR interval |
| **Breslow-Day** | Test homogeneity of odds ratios |
| **Correspondence analysis** | Dimensionality reduction for tables |
| **Monte Carlo chi-square** | Fixed-margin simulated p-value estimate |
| **Power analysis** | Equal-group, two-sided normal approximation using Cohen's h |
| **Multiple comparisons** | Bonferroni and FDR corrections |

## Installation

### From PyPI (recommended)
```bash
pip install crosstabs
```

### Inspect the public source distribution
```bash
python -m pip download --no-deps --no-binary=:all: crosstabs
```

The development repository is currently private. PyPI publishes the package's
source archive; email
[support@crosstabs.com](mailto:support@crosstabs.com) for issue reports or
source-access questions.

## Quick Start

### Run the MCP Server
```bash
# Focused statistical calculators (Python 3.10+)
crosstabs

# End-to-end research workspace (Python 3.10+ and Node.js 22+)
crosstabs-headless
```

Or directly:
```bash
python -m crosstabs_mcp.server
```

### Configure an MCP client

Add to your `~/.claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "crosstabs_workspace": {
      "command": "crosstabs-headless"
    },
    "crosstabs_statistics": {
      "command": "crosstabs"
    }
  }
}
```

The headless server stores project state and generated artifacts under the local
application-data directory. Deterministic calculations stay on the machine.
It makes no hosted-AI, account, or remote-project call. Manual open-end coding
remains a browser-workspace feature rather than a local MCP operation.

### Headless workflow tools

`create_project`, `import_dataset`, `profile_dataset`, `define_row_set`,
`define_banner`, `apply_filter`, `set_weight`, `define_survey_design`,
`run_table`, `run_tab_book`,
`compare_waves`, `undo_change`, `replace_dataset`, `detect_schema_drift`, `repair_schema`,
`generate_report_pack`, `refresh_report_pack`, `export_project`,
`list_projects`, `inspect_project`, `get_audit_history`, `import_project`,
`render_project_table`, `update_variable_metadata`, `propose_transformation`,
`review_transformation`, `apply_transformation`, `undo_transformation`, and
`run_complex_survey_method`.

Project-package import is an explicit allowed-path, checksummed full-data copy.
It never overwrites a project. Rendering uses a saved table/revision/evidence ID,
not an ordinary re-analysis of caller-supplied cells. Inference-changing edits
retain approval, revision and replay guards. The local JSON store enforces
8 MiB per serialized project (including undo/audit) and 32 MiB per database
(including replay records); parser maxima are not persistence guarantees.

Mutations use expected revisions and idempotency keys. Results include structured
warnings, evidence IDs, audit records, and MCP resources for generated files.
For a detected multi-select row dimension, `define_row_set` may additionally
set `multipleResponseDenominator` to `cases` or `responses`; ordinary and
compound row sets reject that option rather than silently applying it.

### Monte Carlo workload envelope

Fixed-margin Monte Carlo accepts at most 100 rows, 100 columns, and 1,000 cells,
with a total frequency of 9,007,199,254,740,991. It admits 100,000 simulations and 20,000,000 cell-simulations.
It samples chunks of at most 512 simulations under a 32 MiB estimated numeric
peak and a 30-second server deadline.
The returned `workload` object records the admitted dimensions, cell-simulation
product, chunk size, memory estimate, memory ceiling, and deadline. The memory
estimate covers the numeric simulation path, not the MCP transport's parsed JSON
body. A supplied seed is reproducible for the same table, simulation count, and
package versions. MCP cancellation is observed between chunks; an individual
SciPy draw cannot be interrupted mid-chunk.

## Usage Examples

Once configured, Claude can perform statistical analysis:

### Chi-square Test
```
User: Test if there's an association between treatment and outcome:
      Treatment A: 50 success, 30 failure
      Treatment B: 20 success, 40 failure

Claude: [Uses chi_square_test with matrix [[50,30],[20,40]]]
        χ² = 11.67, p = 0.0006
        Cramér's V = 0.29 (small-medium effect)
        There is a significant association between treatment and outcome.
```

### Odds Ratio
```
User: Compare an adverse outcome between exposure groups:
      Exposed: 30 outcome-present, 70 outcome-absent
      Unexposed: 15 outcome-present, 85 outcome-absent

Claude: [Uses odds_ratio with matrix [[30,70],[15,85]]]
        OR = 2.43 (95% CI: 1.21-4.87)
        Exposure is associated with 143% higher odds of the outcome.
```

The epidemiology tools (`odds_ratio`, `relative_risk`, `risk_difference`, and
`attributable_risk`) use one explicit orientation:
`[[exposed outcome+, exposed outcome-], [unexposed outcome+, unexposed outcome-]]`.
They return machine-readable zero/infinite/undefined states rather than silently
continuity-correcting point estimates. Attributable/prevented fractions require
causal identification assumptions; an association alone does not establish the
counterfactual effect of removing an exposure.

### Fisher's Exact Test
```
User: I have a small sample: [[3,1],[1,5]]. Is it significant?

Claude: [Uses fishers_exact with the matrix]
        p = 0.190476 (two-tailed exact)
        Not statistically significant at α=0.05.
```

## Available Tools

| Tool Name | Description |
|-----------|-------------|
| `chi_square_test` | Chi-square test of independence |
| `g_test` | G-test (likelihood ratio) |
| `fishers_exact` | Fisher's exact test (2×2) |
| `mcnemar_test` | McNemar's test for paired data |
| `odds_ratio` | Odds ratio with CI |
| `relative_risk` | Relative risk with CI |
| `risk_difference` | Risk difference with CI |
| `cramers_v` | Cramér's V effect size |
| `phi_coefficient` | Phi for 2×2 tables |
| `cohens_kappa` | Cohen's kappa |
| `weighted_kappa` | Weighted kappa |
| `spearmans_rho` | Spearman's rank correlation |
| `kendalls_tau` | Kendall's tau-b |
| `goodman_kruskal_gamma` | Gamma coefficient |
| `somers_d` | Somers' D |
| `tau_c` | Stuart's tau-c |
| `cmh_test` | Cochran-Mantel-Haenszel |
| `breslow_day_test` | Breslow-Day test |
| `linear_trend_test` | Linear-by-linear association |
| `correspondence_analysis` | Correspondence analysis |
| `monte_carlo_chi_square` | Fixed-margin Monte Carlo p-value estimate |
| `power_analysis` | Two-sided normal-approximation power/sample size using Cohen's h |
| `bonferroni_correction` | Bonferroni p-value adjustment |
| `fdr_correction` | Benjamini-Hochberg FDR |
| `standardized_residuals` | Cell residuals |
| `post_hoc_chi_square` | Post-hoc chi-square decomposition |
| `proportion_ci` | Confidence interval for proportion |
| `check_assumptions` | Validate chi-square assumptions |
| `recommend_test` | Method suggestions with assumption caveats |
| `mosaic_plot_data` | Data for mosaic visualization |
| `stacked_bar_data` | Data for stacked bar chart |
| `attributable_risk` | Attributable risk measures |
| `chi_square_yates` | Yates' continuity correction |
| `effect_size` | Multiple contingency-table effect sizes |
| `lambda_coefficient` | Goodman–Kruskal lambda |
| `uncertainty_coefficient` | Theil's uncertainty coefficient |
| `detect_outliers` | Outlier detection |
| `crosstab_from_data` | Build table from raw data |
| `crosstab_from_csv` | Build table from CSV |

## Market-research tools (added in 1.5.0)

These six tools were added in **1.5.0 and are not part of the earlier
1.4.0 distribution**. They are registered on the `crosstabs` statistical server
alongside the 39 statistical tools and runtime status. The tools take
respondent-level rows (one object per respondent) and follow the definitions and defaults of the browser-local
calculators at https://www.crosstabs.com/tools, so both report the same numbers.
They are outside the pending named statistical review scope.

| Market-research tool | Description |
|-----------|-------------|
| `turf_analysis` | TURF reach and frequency: exhaustive up to 50,000 combinations, otherwise greedy, plus an incremental reach ladder |
| `penalty_analysis` | Just-about-right penalty analysis: mean drops, weighted penalties, Welch tests, 20% action line |
| `key_driver_analysis` | Shapley R² decomposition and Johnson's relative weights with VIF and collinearity warnings |
| `nps_analysis` | Net Promoter Score with multinomial standard errors, segment and two-group tests |
| `survey_sample_size` | Completes for a margin of error, margin of error for n, two-proportion group sizes |
| `data_quality_flags` | Speeder, straight-liner, duplicate and incomplete-interview flags; nothing is removed |

## Statistics procedure tools (added in 1.5.0)

These two headless tools were added in **1.5.0 and are not part of the earlier
1.4.0 distribution**. They run in `crosstabs-headless` and bring its catalog
from 35 to 37 headless research workflow tools.

| Headless tool | Description |
|-----------|-------------|
| `list_statistical_procedures` | Catalog of 110 SPSS-equivalent Statistics procedures (id, SPSS command, required inputs, weight support, missing-value handling, randomness, validation references); pass `procedureId` for its options JSON Schema |
| `run_statistical_procedure` | Run one procedure on a local project wave with validated options and an optional saved frequency or sampling weight; returns the engine's structured tables, footnotes, validation references and a deterministic input fingerprint |

The procedures are the same pure TypeScript engines as the browser Statistics
mode, so both report the same numbers. Weights are never guessed: `weightId`
must name a saved project weight declared as `frequency` (exact counts;
ordinary inference) or `sampling` (weighted point estimates; ordinary
significance tests withheld). Unspecified, calibration and analytic weights
are rejected, and procedures that are unweighted reject a weight instead of
ignoring it. Complex Samples procedures run only on a saved survey design
approved through `define_survey_design`; inline designs are rejected. Seeded procedures echo their seed. Three browser procedures that
write sample selections or browser-only plan state (`cs-plan`, `cs-select`,
`sample`) are not exposed.

## Pull from a database (added in 1.5.0)

Browsers cannot talk to databases, and crosstabs.com will not run a hosted
proxy that holds your credentials or respondent rows. `crosstabs pull` runs
**on your own machine**: it connects to your database with your credentials,
runs one read-only query and writes a file that the crosstabs.com workspace
opens locally (`.sav`, `.dta`, `.csv` or `.xlsx`; `.parquet` is also written
for other tools, but the workspace does not open it yet). The query,
credentials and rows never go to crosstabs.com.

Install the `db` extra plus your database's driver. The core `crosstabs`
install does not include them, and a missing one produces an error naming
the extra to install:

| Extra | Database | Driver installed |
|-------|----------|------------------|
| `crosstabs[db]` | SQLite (stdlib) | SQLAlchemy, pyreadstat, pyarrow, sqlglot, PyYAML, openpyxl |
| `crosstabs[postgres]` | PostgreSQL | psycopg 3 |
| `crosstabs[redshift]` | Amazon Redshift (PostgreSQL protocol) | psycopg 3 |
| `crosstabs[mysql]` | MySQL / MariaDB | PyMySQL |
| `crosstabs[mssql]` | SQL Server / Azure SQL | pyodbc (needs Microsoft ODBC Driver 18 installed separately) |
| `crosstabs[oracle]` | Oracle | python-oracledb (thin mode, no Instant Client needed) |
| `crosstabs[snowflake]` | Snowflake | snowflake-sqlalchemy |
| `crosstabs[databricks]` | Databricks SQL / Unity Catalog | databricks-sqlalchemy, databricks-sql-connector, databricks-sdk |
| `crosstabs[bigquery]` | Google BigQuery | sqlalchemy-bigquery |
| `crosstabs[duckdb]` | DuckDB | duckdb-engine |
| `crosstabs[all-db]` | all of the above | |

Put the SQLAlchemy URL in an environment variable and pass its **name** with
`--url-env` (a `--url` value is visible in shell history and the process list).
Passwords and tokens are masked in every message, log line and error.

```bash
pip install "crosstabs[postgres]"
export CROSSTABS_DB_URL='postgresql://analyst:PASSWORD@db.example.com:5432/surveys'
crosstabs pull --url-env CROSSTABS_DB_URL --table public.responses --out wave3.sav
crosstabs pull --url-env CROSSTABS_DB_URL --query-file wave3.sql \
  --labels labels.yaml --column-comments --out wave3.sav
```

Snowflake (password, key pair or browser SSO):

```bash
pip install "crosstabs[snowflake]"
# Key-pair authentication: the URL holds no secret.
export SNOWFLAKE_URL='snowflake://ANALYST@myorg-myaccount/SURVEYS/PUBLIC?warehouse=ANALYTICS_WH&role=SURVEY_READER'
export SNOWFLAKE_PRIVATE_KEY_PATH="$HOME/.ssh/snowflake_rsa_key.p8"
export SNOWFLAKE_PRIVATE_KEY_PASSPHRASE='...'   # only for an encrypted key
crosstabs pull --url-env SNOWFLAKE_URL --table SURVEYS.PUBLIC.RESPONSES --column-comments --out wave3.sav
# Single sign-on in the system browser:
export SNOWFLAKE_URL='snowflake://me%40example.com@myorg-myaccount/SURVEYS/PUBLIC?warehouse=ANALYTICS_WH&authenticator=externalbrowser'
```

Databricks (personal access token or OAuth machine-to-machine):

```bash
pip install "crosstabs[databricks]"
export DATABRICKS_URL='databricks://token@adb-1234567890.12.azuredatabricks.net?http_path=/sql/1.0/warehouses/abcdef&catalog=main&schema=surveys'
export DATABRICKS_TOKEN='dapi...'                 # personal access token, or:
export DATABRICKS_CLIENT_ID='...' DATABRICKS_CLIENT_SECRET='...'   # service principal (OAuth M2M)
crosstabs pull --url-env DATABRICKS_URL --table main.surveys.responses --column-comments --out wave3.sav
```

A token may also be written in the URL (`databricks://token:dapi...@host?...`).
Unity Catalog three-part names (`catalog.schema.table`) work with `--table`
and in queries.

BigQuery and SQL Server:

```bash
pip install "crosstabs[bigquery]"
gcloud auth application-default login         # or GOOGLE_APPLICATION_CREDENTIALS=key.json
export BQ_URL='bigquery://my-project/surveys'
crosstabs pull --url-env BQ_URL --query "SELECT * FROM surveys.responses WHERE wave = 3" --out wave3.sav

pip install "crosstabs[mssql]"                # plus Microsoft ODBC Driver 18 for SQL Server
export MSSQL_URL='mssql+pyodbc://analyst:PASSWORD@sql.example.com:1433/Surveys?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=yes'
crosstabs pull --url-env MSSQL_URL --table dbo.Responses --out wave3.sav
```

Other URL forms: `mysql://user:pw@host/db`, `oracle://user:pw@host:1521/?service_name=ORCL`,
`redshift://user:pw@cluster.region.redshift.amazonaws.com:5439/dev`,
`duckdb:///path/survey.duckdb`, `sqlite:///path/survey.db`.

**Read-only by design.** Only a single `SELECT` or `WITH` statement is
accepted (parsed with sqlglot; a conservative keyword check is used if sqlglot
is missing). `INSERT`, `UPDATE`, `DELETE`, `DROP`, DDL, `SELECT ... INTO`,
`FOR UPDATE`, data-modifying CTEs and known side-effect functions are refused.
The session is also opened read-only where the database supports it (SQLite,
DuckDB, PostgreSQL, MySQL, Oracle), a statement timeout is set (`--timeout`,
default 300 seconds) and the transaction is rolled back. Snowflake, Databricks,
BigQuery and SQL Server have no read-only session switch, so use a login or
role with SELECT-only grants; that grant is the real security boundary.

**Limits.** Results are refused above 1,000,000 rows, 1,000 columns or
20,000,000 cells, the same limits the browser import applies. `--limit N`
keeps the first N rows instead and the summary reports `truncated: true`.

**Types.** Integers, floats, text, booleans (1/0 in `.sav`/`.dta`), dates,
timestamps (time-zone-aware values converted to UTC) and times are preserved;
decimals become floating point with a note; NULL becomes system-missing (and
an empty string for text in `.sav`/`.dta`, which have no missing text value).
JSON, binary and UUID values are stored as text with a note. Nothing is
recoded and no weight is guessed: text is coded 1..k with value labels only
for columns named in `--categorical` or marked `categorical` in the labels
file. Column names that SPSS/Stata cannot use are renamed, with the original
kept as the variable label and reported in the summary. Variable labels can
come from database column comments with `--column-comments` and `--table`
(PostgreSQL, MySQL, Oracle, SQL Server, Snowflake `COMMENT`, Databricks and
DuckDB comments; BigQuery column descriptions).

Labels file (`--labels labels.yaml`, or `.json`):

```yaml
variables:
  resp_id: Respondent ID              # a string is a variable label
  q1:
    label: Overall satisfaction
    measure: ordinal                  # nominal | ordinal | scale
    values: {1: Very poor, 5: Excellent}
    missing: [99]                     # user-missing codes (.sav only, up to 3)
  tier:                               # a text column coded on request
    categorical: true
    order: [low, medium, high]        # codes 1, 2, 3; unlisted values are an error
```

The same engine is available to agents on the statistical MCP server. The tool
takes the **name** of the environment variable that holds the URL, never the
URL or a password:

| Database tool | Description |
|-----------|-------------|
| `pull_database_query` | Run one read-only SELECT/WITH query (or read a table) through a connection URL held in a named environment variable, and write a local `.sav`/`.dta`/`.csv`/`.xlsx`/`.parquet` file; returns the path, row and column counts, per-column schema and the read-only protections applied |

Set the variable in the MCP client's server configuration (`"env":
{"CROSSTABS_DB_URL": "..."}`) and call the tool with
`connectionEnvVar="CROSSTABS_DB_URL"` and an absolute `outputPath`. Existing
files are not overwritten unless `overwrite=true`.

## Pull from a survey platform (added in 1.5.0)

`crosstabs pull <platform>` downloads responses from your survey-platform
account **on your own machine** and writes a labelled file the crosstabs.com
workspace opens (`.sav` keeps every label; `.dta`, `.csv`, `.xlsx` and
`.parquet` are also written). Credentials are read from environment variables
only: there is no flag or tool argument that takes a token, key, secret or
password, and every message masks them. Responses never go to crosstabs.com.

```bash
pip install "crosstabs[surveys]"   # pyreadstat, pyarrow, openpyxl, google-auth-oauthlib
crosstabs pull qualtrics --help    # credentials, settings and an example per platform
```

Every platform is mapped onto one model:

- question wording becomes the variable label and choices become value labels
  (platform recode values are kept when they are numeric and unique,
  otherwise choices are numbered 1..n in display order);
- a single choice or NPS question is one numeric variable; a multi-select is one
  0/1 variable per choice (`Q2_1`, `Q2_2`, ...; "Not selected"/"Selected")
  grouped as a dichotomous multiple-response set; a grid is one variable per
  row; a multi-answer grid is one set per row; a ranking is one rank variable
  per item; "other, please specify" text is a `_other` text variable;
- only complete responses are kept unless you pass `--include-partial`, and
  `response_complete` (1 complete, 0 partial) is always written;
- weights are never guessed or applied: a platform weight field (embedded data,
  hidden field, calculated field) is carried as a plain numeric variable;
- IP address, location and contact fields are left out unless you pass
  `--include-identifiers`.

Next to the data file, `<name>.survey.json` records the platform, survey,
export time, filters, response counts, question definitions and the
multiple-response sets (in the same shape `pyreadstat.read_sav(...).mr_sets`
returns; pyreadstat cannot write SPSS MR-set records). The workspace finds the
sets again on import because their variables follow the `<question>_<n>` 0/1
pattern its multi-select detector recognises. Results above 1,000,000
responses, 1,000 variables or 20,000,000 cells are refused, as for the
database pull.

| Platform | Credentials (environment) | Example |
|----------|---------------------------|---------|
| Qualtrics | `QUALTRICS_API_TOKEN`, or `QUALTRICS_CLIENT_ID` + `QUALTRICS_CLIENT_SECRET` (OAuth client credentials) | `crosstabs pull qualtrics --survey SV_abc123XYZ --datacenter iad1 --out wave3.sav` |
| SurveyMonkey | `SURVEYMONKEY_ACCESS_TOKEN` | `crosstabs pull surveymonkey --survey 405060708 --region eu --out pulse.sav` |
| Medallia Experience Cloud | `MEDALLIA_CLIENT_ID` + `MEDALLIA_CLIENT_SECRET` (or `MEDALLIA_ACCESS_TOKEN`); `MEDALLIA_TOKEN_URL`, `MEDALLIA_QUERY_URL` | `crosstabs pull medallia --survey relationship-2026 --fields e_responsedate,e_ltr,q_osat --since 2026-01-01 --out cx.sav` |
| Alchemer | `ALCHEMER_API_TOKEN` + `ALCHEMER_API_TOKEN_SECRET` | `crosstabs pull alchemer --survey 7654321 --region us --out event.sav` |
| Typeform | `TYPEFORM_TOKEN` | `crosstabs pull typeform --survey aBcD1234 --include-partial --out feedback.sav` |
| Google Forms | `GOOGLE_OAUTH_CLIENT_SECRETS` (desktop client JSON path; browser sign-in on 127.0.0.1), optional `GOOGLE_OAUTH_TOKEN_CACHE`, or `GOOGLE_OAUTH_ACCESS_TOKEN` | `crosstabs pull google-forms --survey 1FAIpQLSf... --out volunteers.sav` |
| Forsta Surveys (Decipher) | `DECIPHER_API_KEY`; `DECIPHER_SERVER` | `crosstabs pull decipher --survey selfserve/53b/proj1234 --server selfserve.decipherinc.com --out tracker.sav` |
| QuestionPro | `QUESTIONPRO_API_KEY` | `crosstabs pull questionpro --survey 11223344 --region us --out staff.sav` |
| Jotform | `JOTFORM_API_KEY` | `crosstabs pull jotform --survey 241234567890123 --out intake.sav` |
| SurveySparrow | `SURVEYSPARROW_TOKEN` | `crosstabs pull surveysparrow --survey 555 --out nps.sav` |
| REDCap | `REDCAP_API_TOKEN`; `REDCAP_API_URL` | `crosstabs pull redcap --survey cohort --api-url https://redcap.example.edu/api/ --forms baseline --out cohort.sav` |
| KoboToolbox | `KOBO_API_TOKEN`; `KOBO_SERVER` | `crosstabs pull kobo --survey aXyZ123abc --server eu.kobotoolbox.org --out household.sav` |
| ODK Central | `ODK_CENTRAL_TOKEN`, or `ODK_CENTRAL_EMAIL` + `ODK_CENTRAL_PASSWORD`, or `ODK_CENTRAL_APP_USER_TOKEN`; `ODK_CENTRAL_URL` | `crosstabs pull odk-central --survey 3/water_audit --server https://central.example.org --out audit.sav` |
| LimeSurvey | `LIMESURVEY_USERNAME` + `LIMESURVEY_PASSWORD`; `LIMESURVEY_API_URL` | `crosstabs pull limesurvey --survey 123456 --api-url https://survey.example.org/index.php/admin/remotecontrol --out commute.sav` |

Platform notes:

- **Qualtrics** uses the survey-definitions API and an asynchronous JSON export
  (start, poll, download zip). `--include-partial` adds a second export of
  in-progress responses. Export tags name the variables; loop & merge
  iterations become `1_Q9`, `2_Q9` (`v1_Q9` in `.sav`); embedded data is kept.
  Side-by-side, heat-map, drill-down and other rich types are listed in the
  notes and not exported.
- **SurveyMonkey** follows `/responses/bulk` `links.next` pages and backs off on
  HTTP 429 using `Retry-After` or the `X-Ratelimit-App-Global-Minute-Reset` header.
  Disqualified and over-quota responses are never kept.
- **Medallia** depends on your contract: the Query API endpoint, the OAuth token
  URL, the fields your data-access role may read and the rate limits are set
  per instance. Pass the field ids with `--fields`; value labels come from the
  labels returned with each value. The `feedback` object only holds completed
  records, so `--include-partial` has no effect.
- **REDCap** parses `"1, Yes | 2, No"` choices; checkboxes keep REDCap's
  `field___code` names (minus signs become `_`); a record is complete when every
  `<instrument>_complete` field present is 2.
- **Decipher** reads the JSON datamap for labels and the data API with
  `cond=qualified` (with `--include-partial` status 4 is kept as partial;
  terminated and over-quota records are never kept). `--layout` passes a layout id.
- **Google Forms**, **Jotform**, **KoboToolbox** and **ODK Central** only return
  submitted responses. ODK labels and choices come from the XForm (itext and
  secondary instances); repeat groups are not flattened (Kobo and ODK).
- QuestionPro, SurveySparrow and LimeSurvey connectors are thinner: they cover
  the documented list/response endpoints and the common question types.

The same engine is available to agents on the statistical MCP server. The tool
takes the platform, survey id, an absolute output path and non-secret settings;
credentials come from the MCP server's environment (`"env":
{"QUALTRICS_API_TOKEN": "..."}` in the client configuration):

| Survey tool | Description |
|-----------|-------------|
| `pull_survey_responses` | Pull responses from one of 14 survey platforms with credentials from environment variables and write a local labelled `.sav`/`.dta`/`.csv`/`.xlsx`/`.parquet` file plus a `.survey.json` sidecar; returns the paths, complete/partial counts, multiple-response sets, per-variable schema and notes |

Existing files are not overwritten unless `overwrite=true`. Google Forms sign-in
opens a browser on the machine running the server unless
`GOOGLE_OAUTH_ACCESS_TOKEN` is set.

## Connect crosstabs.com to a database or survey platform (local bridge)

`crosstabs connect` runs a small bridge on your own computer so the
crosstabs.com workspace can open a database table, a query result, survey
responses or a CSV file without any Crosstabs server in between.

```bash
pip install "crosstabs[db]"            # plus e.g. crosstabs[postgres] for other databases
crosstabs connect --sqlite survey.db   # or --duckdb, --url-env CROSSTABS_DB_URL, --csv-dir ~/exports
```

The bridge prints a port and a one-time pairing code. In the workspace choose
**Use the local bridge**, enter the port and select **Connect**: a window
served by the bridge opens, you type the pairing code there, pick a table or
write one SELECT query, preview it and send it. The rows go from that window
to the crosstabs.com window by `postMessage`; the workspace then runs its
normal import review.

- Listens on `127.0.0.1` only (default port 47613, or a free port if taken);
  requests with any other `Host` header are refused.
- crosstabs.com never fetches the bridge, so there are no CORS headers, no
  mixed-content subresources and no Local Network Access subresource requests.
- Data is posted only to `https://www.crosstabs.com` or `https://crosstabs.com`
  (`--allow-vercel-previews` adds crosstabs-lite preview deployments, `--dev`
  adds `http(s)://localhost:<port>`).
- Credentials come from environment variables (`CROSSTABS_*`, or names given
  with `--url-env` / `--allow-env`) or are typed into the bridge window and kept
  in memory; they are never put in a URL or sent to crosstabs.com.
- Queries go through the `crosstabs pull` engine: single SELECT/WITH only,
  read-only session where supported, statement timeout, credential redaction,
  and the browser import limits (50 MB per transfer).
- Survey platforms (the 14 `crosstabs pull <platform>` sources) are added in
  the bridge window with **Add a connection**; tokens come from the platform's
  environment variables or are typed there and kept in memory. Responses travel
  as a labelled `.sav`, and multiple-response set definitions from the
  `.survey.json` sidecar travel with them so the workspace keeps the sets and
  their choice wording.
- Stops after 30 minutes without requests (`--idle-timeout`).

Further source kinds register through
`crosstabs_mcp.bridge_sources.register_source_kind`. The threat model is in
the repository's `docs/local-connection-bridge.md`.

## Changes in 1.5.0

- Statistical server (`crosstabs`): adds six market-research tools
  (`turf_analysis`, `penalty_analysis`, `key_driver_analysis`, `nps_analysis`,
  `survey_sample_size`, `data_quality_flags`) and the local read-only database
  pull `pull_database_query` (also `crosstabs pull`) and the survey-platform pull
  `pull_survey_responses` (also `crosstabs pull <platform>`), for 48 registered tools. The 39
  statistical tools, their input-semantics contracts and the phase-1 review
  scope are unchanged; the market-research, database and survey tools sit outside
  that scope. Database drivers are optional extras (`crosstabs[db]`,
  `crosstabs[postgres]`, `crosstabs[snowflake]`, `crosstabs[databricks]`, ...);
  the survey pull's file writers and Google sign-in are in `crosstabs[surveys]`.
- Headless server (`crosstabs-headless`): adds `list_statistical_procedures`
  and `run_statistical_procedure` (110 SPSS-equivalent Statistics procedures
  using the browser Statistics-mode engines), for 37 tools. Weights must be a
  saved frequency or sampling weight; Complex Samples procedures require a
  survey design approved through `define_survey_design`.
- `crosstabs connect`: a loopback-only connection bridge that hands database
  tables, query results and CSV-folder files to the crosstabs.com workspace by
  `postMessage` (no MCP tool is added).
- Both servers report package version 1.5.0 through `get_runtime_status`.

## Development

### Run Tests
```bash
pip install -e ".[dev]"
pytest tests/ -v
```

### Project Structure
```
mcp-server-python/
├── crosstabs_mcp/
│   ├── __init__.py
│   ├── headless_launcher.py # Node version check and bundled-server launcher
│   ├── headless-mcp.mjs     # Local-first 37-tool workflow MCP server
│   ├── server.py          # Main MCP server
│   ├── market_research.py # Market-research analyses (since 1.5.0)
│   ├── db_connector.py    # Local read-only database pull (since 1.5.0)
│   ├── cli.py             # `crosstabs pull` / `crosstabs connect` subcommands
│   ├── bridge.py          # Loopback connection bridge for crosstabs.com
│   ├── bridge_sources.py  # Bridge source interface: database, CSV folder, survey platforms
│   ├── bridge_static/     # The bridge's own pairing and source page
│   ├── survey_cli.py      # `crosstabs pull <platform>` (since 1.5.0)
│   ├── survey_sources/    # One module per survey platform + shared model/writer
│   └── advanced_stats.py  # Compatibility statistical kernels and contracts
├── tests/
│   ├── test_statistics.py       # Statistical behavior tests
│   ├── test_reference_parity.py # Public SciPy reference parity
│   ├── test_market_research.py  # Market-research tools and website parity
│   ├── test_db_connector.py     # Database pull: SQLite/DuckDB end to end, read-only, limits
│   ├── test_bridge*.py          # Bridge: loopback, Host/Origin checks, pairing, payloads, survey adapter, idle stop
│   └── test_survey_*.py         # Survey pull: recorded platform payloads, .sav round trips
├── scripts/                    # Distribution verification
├── LICENSE
├── pyproject.toml
├── uv.lock
└── README.md
```

## Requirements

- Python 3.10+
- mcp >= 1.0.0
- fastmcp >= 0.1.0
- numpy >= 1.24.0
- scipy >= 1.10.0
- pandas >= 2.0.0
- statsmodels >= 0.14.0

## License

MIT License - see [LICENSE](LICENSE) for details.

## Contributing

The development repository is currently private. Send corrections and proposed
changes to [support@crosstabs.com](mailto:support@crosstabs.com).

## Links

- [PyPI package and source archive](https://pypi.org/project/crosstabs/#files)
- [Web Application](https://www.crosstabs.com)
- [Support](https://www.crosstabs.com/support)
- [MCP Documentation](https://modelcontextprotocol.io)
