Exactly explainable classifiers for agents: train, tune, predict, explain on CSV or Excel.
Runs on your machine from its package; the steps are below.
At most: creates or changes data: records, files, settings.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx glin-ml
MCP client config
{
"mcpServers": {
"glin": {
"command": "uvx",
"args": [
"glin-ml"
]
}
}
}☕ If glin is useful to you and you want to help maintain it, you can buy me a coffee.
glin is an MCP plugin. It gives AI agents a fast, statistical "gut feeling" (System 1 thinking). You install it into an agent such as Claude Code, Claude Desktop or Cursor. Then you talk to the agent in plain language. The agent calls the glin tools.
glin trains an Explainable Boosting Machine (EBM) on a CSV or Excel file. Each prediction comes with an exact breakdown of the features that drove it. The breakdown comes from the additive structure of the model. It is not a SHAP or LIME approximation.
To use glin, you do not write code and you do not type glin commands. The agent trains, tunes, predicts and explains. The server runs on your machine and talks to the agent over stdio.
glin runs locally only (stdio). It has no remote or HTTP mode. Version 0.1.3 is the last release that had one.
~/data/leads.csv that predicts converted."See Example: ask Claude directly for a full conversation.
Changelog: CHANGELOG.md lists the changes in each release.
Case study: The segment of 100,000 sales leads. Claude with glin put 71% of new leads in the correct segment. Claude alone got 33%. With 1,000 labeled leads in the prompt, Claude got 60% and used 6.7 times more tokens.
csv_pathglin needs uv on your machine. The MCP client starts the server with uvx. You do not install glin yourself.
The server code is the PyPI package glin-ml. The name glin belongs to an unrelated project. uvx downloads glin-ml the first time the client starts the server, and caches it.
The plugin adds the glin MCP server and a skill. The skill tells the agent when and how to build, tune, predict with and explain a model. The plugin runs uvx --from 'glin-ml[excel]>=0.1.4' glin serve. This command includes Excel support, and it does not start a version older than 0.1.4.
In Claude Code, run:
/plugin marketplace add AkashChatterjee/glin
/plugin install glin@glin-marketplace
The files are in .claude-plugin/marketplace.json and plugins/glin/.
Add glin to the config of your MCP client, for example Claude Desktop or Cursor. In Claude Desktop, the file is claude_desktop_config.json:
{
"mcpServers": {
"glin": {
"command": "uvx",
"args": ["--from", "glin-ml[excel]", "glin", "serve"]
}
}
}
Restart the client. The glin tools and prompts then appear. The [excel] extra adds support for .xlsx and .xls files. Remove it if you only use CSV files. See Excel input.
The server gives the glin version in serverInfo.version. glin serve writes only MCP messages to stdout. Log messages go to stderr. During serve, glin sets the log level of the interpret library to WARNING, so each training does not fill the client log.
To add server flags such as --allow-dir, put them after serve in args. See Restricting csv_path.
To run glin from a copy that is already installed (uv tool install glin-ml, pipx install glin-ml or pip install glin-ml), set "command": "glin" and "args": ["serve"].
After each release, the maintainer publishes glin to the official MCP Registry as io.github.AkashChatterjee/glin. This is a manual step (see Publishing to the MCP Registry). The metadata is in server.json. The registry points to the glin-ml package on PyPI. The package also installs a glin-ml command, which is the same as glin, so uvx glin-ml serve works.
The registry entry installs glin-ml without the excel extra, because the registry format has no field for extras. To read Excel files, set your MCP client to run uvx --from 'glin-ml[excel]' glin serve (command uvx, arguments --from, glin-ml[excel], glin, serve).
Install an editable copy from a clone of this repository:
git clone https://github.com/AkashChatterjee/glin.git
cd glin
pip install -e ".[dev]" # also installs the Excel libraries, and xlwt to build .xls test files
To try the checkout in an MCP client, set "command" to the glin program in your environment (for example /path/to/glin/.venv/bin/glin) and "args" to ["serve"].
glin works with the mcp 1.x and 2.x Python SDK lines (mcp>=1.21.1,<3). Versions before 1.21.1 do not work. mcp 1.21.0 and earlier fail on import with pydantic 2.14 and later. The earliest 1.x versions also do not have the API that glin uses. To test one line, add a constraint. For example: pip install -e ".[dev]" "mcp>=2,<3".
The Tests GitHub Actions workflow (.github/workflows/test.yml) runs the test suite on each pull request and on each push to main. It runs on Python 3.10 and 3.12 with the latest mcp 1.x and the latest mcp 2.x. It also runs on Python 3.10 with the lowest supported mcp (mcp==1.21.1).
Ask the agent. Give it a data file and the column to predict:
"Train a classifier on
~/data/churn.csvthat predictschurn. Call itchurn_v1."
The agent calls train_model. For a careful build, ask the agent to use the build_classifier prompt. It cleans the data, drops leakage, tunes the model and reports the honest performance. See Prompts exposed over MCP.
glin saves each model in ~/.glin/models/<name>/.
When you make a model (model_name in train_model, or glin train --name), the name must obey this rule:
-, _ and .... Thus ., .. and hidden names are not valid./ or \. Thus a path or an absolute path is not valid.When you use or delete a model (inspect_model, predict, explain_model, glin delete), the name must be one folder name in the models folder. It must not contain / or \, and it must not be empty, . or ... Thus you can still use models that an earlier glin version saved with other names, for example a name with a space.
glin rejects a name that does not obey the applicable rule. The error message gives the rule. glin also makes sure that the model folder is directly in the models folder, also when a symbolic link is in the path. Thus a model name cannot write, read or delete files outside the models folder.
.xlsx / .xls)Spreadsheets from governments and analysts train directly. This includes banner rows and merged headers. Excel support is an optional extra, so the base install stays small. openpyxl reads .xlsx files. xlrd reads old .xls files. The Claude Code plugin includes the extra. For other clients, use glin-ml[excel] in the server command (see Other MCP clients).
Ask the agent in plain language, for example:
"Train a classifier on
~/data/report.xlsx, sheetData 2023, header rows 3-4, that predictsoutcome."
The agent passes sheet and header_rows to train_model. The rules below apply to both. The flags --sheet and --header-rows are the command line forms of these arguments:
glin train report.xlsx --target outcome --name report_v1 # one sheet
glin train report.xlsx --sheet "Data 2023" --target outcome --name r_v1 # name, or 1-based position
glin train report.xlsx --sheet 2 --header-rows 3-4 --target outcome --name r_v1
--sheet takes a name or a 1-based position. A workbook with one sheet needs no flag. A workbook with several sheets and no --sheet gives an error that lists the sheets. glin ignores case and spaces at the start and end of a name only if one sheet matches. If two or more sheets match (for example Data and data ), glin gives an error that lists them. Then use the exact name or the position.--header-rows, glin finds the first row that is mostly filled and looks like labels. It skips title rows above it. If the choice is not clear, glin stops with an error. The error names the rows and asks for --header-rows. glin does not guess. Two cases are not clear. In the first, a sparse group-label row sits next to the header row. In the second, the first full row looks like data.Source, Note, Footnote, *, †, ‡ or ©, or it is a sentence of six words or more. glin does the same for all column counts. A sparse last row that does not look like a note stays as data. glin gives a warning with the sheet row numbers of the rows that it dropped.--header-rows: Use 1-based row numbers as shown in Excel: 4, 3-4 or 3,4. glin joins a multi-row header into one name for each column with -, for example 2023 - sales. A blank cell in an upper header row continues the label from the cell on its left (merged group headers). glin drops blanks and repeated parts. Duplicate names get the suffixes _2 and _3. A header cell that holds a date becomes an ISO date, for example 2024-01-01. Data starts below the last header row. glin prints the sheet, the header rows and the number of rows it read.--sheet and --header-rows are errors for CSV files.The glin program is mainly the MCP server (glin serve). It also has small helpers for a terminal. You do not need them to use glin. They are useful for scripts, and for checks outside an agent. Install glin first (uv tool install glin-ml).
The agent can list models (list_models). It has no tool to delete a model. To delete one, use glin delete or remove the folder in ~/.glin/models/.
glin --help
glin --version
glin train path/to/data.csv --target churn --name churn_v1
glin list
glin delete churn_v1 # asks for confirmation
glin delete churn_v1 --yes # skips the question
During training, glin train prints a live line that shows the boosting progress. To stop this line, add --quiet. The stage messages still print. glin train trains on every row with the default hyperparameters, and it has no holdout.
Every tool has MCP annotations (title, readOnlyHint, destructiveHint). All tools are read-only except train_model. train_model is marked destructive because overwrite=True replaces a model. A client can approve the read-only tools automatically and ask before it runs train_model.
list_models() lists all trained models.inspect_model(model_name) returns the feature schema and the target classes. It lists date columns under features.date. Each date column shows its derived terms and the start_date that days_since_start counts from.predict(model_name, features, top_n=10) returns the predicted class and the probabilities, plus a full audit:
base_logit is the intercept.contributions lists the contribution of every term, sorted by size.additivity_verified checks that reconstructed_probability equals model_probability. Both are keyed by class name.score_class is the second entry of target_classes. glin sorts the classes by value, so this is not always the alphabetically second class. The scores and base_logit are log-odds for this class. A positive score pushes toward it. A negative score pushes toward the other class.predicted_class, score_class and the entries of target_classes have the type of the target values (for example 1, or true in JSON). The keys of probabilities, model_probability and reconstructed_probability are always text (for example "1" or "True"). To find the probability of score_class in Python, use model_probability[str(score_class)].base_logit is per class, and each term has score_by_class.warnings lists inputs that the model never saw. Each warning has feature, value and issue. The issue is unseen_category, out_of_range, unparseable_number, unparseable_date, ambiguous_date_format or date_order_mismatch. out_of_range warnings also have training_range. Date format warnings also have parsed_as, the date that glin used. unparseable_number is for a numeric input that is not a finite number, for example abc or inf. glin then treats the input as missing. A blank input or a suppression code does not give this warning. Each value is safe for JSON: glin sends inf as the string "inf".missing_features lists inputs that are absent or blank. These inputs are valid. glin reports them separately."signup_date": "2024-03-06". glin derives the same signup_date__* terms as at training time. Contributions name each term, for example signup_date__day_of_week = sat. A date that is absent, blank or not parseable counts as missing. A date that does not parse also adds a warning.train_model(target_column, model_name, csv_path=None, csv_content=None, overwrite=False, hyperparameters=None, holdout_fraction=0.2, holdout_group_column=None, suppression_codes=None, sheet=None, header_rows=None) trains a new model. It can also retrain an existing model on new data with overwrite=True. If the name exists and overwrite is not set, the call fails. Nothing is replaced by accident.
csv_path and csv_content.csv_path is a .csv, .xlsx or .xls file that the server can read. See Restricting csv_path. Use it for local stdio setups. It works for large files (up to 200 MB by default), and the agent never copies the file contents.csv_content is the raw CSV text. Use it only when the client and the server share no file system, and only for small data. Tens of thousands of rows in a tool argument are slow and use a lot of context.holdout_group_column makes the holdout a grouped holdout.suppression_codes replaces the tokens that count as missing in numeric columns. The default is ["SUPP", "NE", "NP", "z", "*", ".."]. See Feature columns: supported today.glin-ml[excel]), two more arguments apply. sheet is a name or a 1-based position. It is required for workbooks with several sheets. header_rows is 4, "3-4" or [3, 4]. glin detects it if you omit it. See Excel input. The result shows the sheet and header_rows that glin used.hyperparameters, training_seconds and the term counts. date_columns maps each expanded date column to its derived terms. If holdout_fraction > 0, the result also has an evaluation block of out-of-sample metrics. See Let an agent tune the model.warnings has the reason for each dropped column and any "Possible target leakage" entries. See Target leakage check.explain_model(model_name, top_n=15, include_shapes=True) shows what the model learned. glin reads it exactly from the additive structure. It does not sample example predictions. The result has:
21140, not 2.114e+04).list_hyperparameters() lists every EBM hyperparameter. Each entry has its type, default, allowed range and a note on what a higher or lower value does.build_classifier(csv_path="", target_column="", model_name="") runs the whole workflow for any dataset in one prompt:
Select it in the prompt picker of your MCP client. Claude Desktop lists server prompts in the attach (+) menu. Give it a file path and a target. All other arguments are optional. The prompt is self-contained, because clients show prompts to the user and do not let the agent fetch them.
tune_model_hyperparameters(model_name="", target_column="") runs the tuning loop in Let an agent tune the model. It includes the live hyperparameter table. glin reads the defaults and ranges from the code.
prepare_csv_for_training(target_column="") is a checklist for an agent to follow before it calls train_model. It covers:
glin reads this guidance from the actual defaults of the preprocessor, so it cannot drift from the code.
csv_pathtrain_model(csv_path=...) reads only a regular file that ends in .csv, .xlsx or .xls. The name csv_path stays for compatibility, and it also takes Excel files. The fully resolved path (symlinks and .. followed) must be inside an allowed directory. glin rejects directories, devices, other extensions, missing files, path traversal and symlinks that point outside. The error never includes file contents.
By default, the allowed directories are:
/) and not a parent of the home directory (for example /Users).Some clients, for example Claude Desktop on macOS, start the server from /. Then only the home directory is allowed. Claude Code starts the server from the project directory, so that directory is also allowed. To set the directories yourself, use a repeatable flag:
glin serve --allow-dir /data/csvs --allow-dir /srv/uploads
If an allowed directory is /, glin writes a warning to stderr, because then csv_path can read any .csv, .xlsx or .xls file on the machine. In Python, build_server(allowed_dirs=[]) allows no directories. Then csv_path is disabled and only csv_content works.
glin rejects a file that is larger than 200 MB. It checks the size before it reads the file. To change the limit, use --max-file-mb, for example glin serve --max-file-mb 500.
csv_content is not affected by these rules. In the config of your MCP client, add the flags to args, for example ["serve", "--allow-dir", "/data/csvs"].
These checks have two known limits. Both need write access inside an allowed directory:
Thus, do not allow a directory that untrusted users or processes can write to.
The older flag --mode stdio is still accepted so that existing client configs keep working. glin ignores it and writes one deprecation notice to stderr. You can remove it from args. --mode http and --mode sse stop with exit code 2 and this message: "HTTP mode was removed in v0.1.4; v0.1.3 is the last release with it. glin runs over stdio only."
An EBM is a sum of one learned curve for each feature, plus a few pairwise interaction tables. Cyclic boosting with tiny single-feature trees fits it. Most of its settings trade flexibility against smoothness. train_model(hyperparameters={...}) exposes them: max_bins, max_interaction_bins, interactions, learning_rate, max_rounds, early_stopping_rounds, validation_size, outer_bags, inner_bags, min_samples_leaf, max_leaves, reg_alpha, reg_lambda, smoothing_rounds, greedy_ratio and random_state. Settings that you omit keep their defaults. glin rejects unknown names, wrong types and values out of range. The error message lists every problem. glin/hyperparameters.py defines each setting once. Add or widen a setting there.
An agent needs a score to optimize. train_model holds out holdout_fraction of the rows and trains on the rest. The holdout is stratified. Its split uses a seed that does not depend on random_state, so every trial is scored on the same rows. The result has an evaluation block:
log_lossbaseline_log_loss (a guess from the class frequencies)train_log_loss (compare it with log_loss to see overfitting or underfitting)accuracybalanced_accuracyroc_aucglin saves the hyperparameters and the metrics in the metadata of the model.
The tune_model_hyperparameters prompt guides the agent through this loop:
holdout_fraction=0.Trials must use a scratch model_name with overwrite=True. glin train on the command line trains on every row with the default hyperparameters.
Rows from the same state, region, customer, site or user look alike. A random holdout puts near-duplicates on both sides, so the metrics are too optimistic. In one real case, a model scored 82% accuracy and 0.89 AUC on a random holdout. It scored 63% and 0.77 on states that it never saw.
Pass holdout_group_column="state" (or any other column) to train_model. glin then holds out whole groups until about holdout_fraction of the rows are in the holdout. The other rows train the model.
The split uses a seed that does not depend on random_state, so every trial uses the same rows. glin tries 25 seeded group orders. It keeps the order whose holdout class shares are closest to the overall class shares.
The group column stays a normal feature. A held-out group therefore takes the unseen-category path at prediction time. glin fits the preprocessing on the training rows only.
glin groups text values in the same way as the preprocessor: it removes spaces at the start and end and changes letters to lowercase. Thus "S06" and "s06 " are one group, and that group is on one side of the split only. Blank text, nan and none are missing values. If two or more spellings become one group, glin returns a warning.
evaluation also reports holdout_group_column, holdout_groups and train_groups. These counts use the normalised groups. Missing values count as one group. glin also saves the column name in the model metadata.
glin rejects these cases with a clear error:
holdout_fraction;If holdout_fraction=0, glin ignores the column and returns a warning.
The build_classifier and tune_model_hyperparameters prompts tell the agent to use this option when such a column exists. They also tell the agent to report this holdout as the honest estimate.
glin train has no holdout, so it has no equivalent.
When a model is trained and the MCP server is attached, talk to Claude in plain language. You do not need to know the tool schema.
"I've got 3 deals to prioritize before quarter close: Northwind Systems ($42k, Opportunity stage, came from a referral, VP contact, owned by Carla Nguyen, 8 touches logged), Summit Retail Group ($8k, still a Lead, paid social source, owned by Brian Kessler), and Anchor Nonprofit ($3k, still a Lead, owned by Frank Suarez, no activity logged yet). Run them through deal_predictor_v1 and tell me which to prioritize."
Claude calls predict once for each deal. It returns a ranked answer with reasons, not only a number:
| Deal | Win probability | Why |
|---|---|---|
| Northwind Systems | 69% | The Opportunity stage, the referral and the VP contact all favor it. The $42k deal size is the one drag. |
| Summit Retail Group | 35% | It is still a Lead, and it came from paid social. Both are large drags with nothing against them. |
| Anchor Nonprofit | 34% | No activity is logged and it is still a Lead. Nothing in the record favors it. |
Note: a field that you do not mention reaches the model as missing. It does not reach the model as "average". Name a rep and a lifecycle stage, and use the categories that the model learned, for example Lead, Opportunity and Customer. This gives a better answer than extra detail in fields that the model already has.
The segment of 100,000 sales leads uses glin on synthetic CRM data with known rules. Because the rules are known, the study can check each prediction, explanation and what-if against the truth.
An earlier study, Child stunting across Indian districts (NFHS-5), uses glin on real public-sector data. A glin model predicts whether a district has high child stunting. The model gives the exact reason for each prediction.
glin train checks your data before it does any work. The data is a CSV, or a sheet of an Excel workbook after header detection. A hard problem stops training with a clear error. An example is a target column with only one class. A soft problem prints a warning, and training continues. An example is a numeric target with many distinct values.
The rules are in glin/validation.py as a flat list. To support a new data shape, add one rule and one preprocessing case. The rest of the pipeline does not change.
In the Python API, a DataFrame can have column names that are not text, for example 0 and 1. glin changes them to text ("0", "1"), and the results use these names. predict accepts either form as a key. Two names that are the same as text, such as 0 and "0", give an error.
| Data shape | What happens |
|---|---|
Numeric (int, float), including NaN | glin passes the values through. EBM puts missing values in their own bin. glin keeps a column with almost all distinct values if a value repeats or the values have decimals, for example rupee amounts. glin also keeps unique whole numbers with large, uneven gaps, for example salaries or epoch seconds. It drops a column of all-distinct whole numbers as an identifier only if the values fill a range, for example a row index or a shuffled 1..N, or if the column name looks like an ID. See "Row identifiers" below. |
Dirty numeric strings (blanks, for example " " for the total charges of a new customer) | glin converts the column to float64 if at least 80% of the non-null values parse as numbers. Cells that do not parse become NaN. |
| Categorical strings, up to 250 unique values | glin lowercases and strips the text. EBM bins each category. Missing and unseen values map to a __missing__ marker. |
Currency and thousands-separated strings ($1,234.56, £1,234, €1.234,56, USD 100, Rs. 100, 100 EUR) | glin removes currency symbols, spaces and common currency codes (USD, EUR, GBP, INR, Rs. and others) before or after the number. Then it converts the column as above. glin reads a decimal comma only when the column has clear European values (1.234,56, 12,5, 1.234.567). A plain 1,234 is a thousands group. glin uses the same rule in training ranges, in check_inputs and at predict time. glin does not change native numbers (int, float). A value with both separators has a clear format: glin uses the last separator as the decimal mark. Thus 1,500.50 and 1.500,50 both become 1500.5 in all columns. In a European column, 1500.5 is 1500.5, but 1.500 is 1500. A column that mixes both styles is parsed as US style, and glin returns a warning. In that column, a European value with only a comma, such as 12,5, becomes 125. |
Negative numbers (-5, −5, (1,234), 1234-) | glin reads the Unicode minus sign (−, common in UK tables), brackets and a trailing minus (common in accounting exports) as a negative sign. |
Infinity (inf, -inf, inf%, a float inf) | glin treats infinity as missing (NaN). train_model returns a warning that gives the number of infinite values. At predict time, an infinite input gives unparseable_number. |
Percent strings (44.98%, 12 %, (5%)) | glin removes the trailing % before it converts the value. The value stays in percentage points: 44.98% becomes 44.98, not 0.4498. This works together with the currency rules. 50% and 50 in one column give the same value. |
Suppression codes in numeric columns (SUPP, NE, NP, z, *, ..) | glin treats these codes as missing (NaN). It ignores them when it decides if a column is numeric, so the column stays numeric. Matching ignores case and surrounding spaces. To change the set, use EBMTabularPreprocessor(suppression_codes=[...]) or train_model(suppression_codes=[...]). A list replaces the default. Categorical columns do not change, so NE as a region stays a category. A numeric column can also have a few short, repeated non-numeric values that are not in the set, for example x or c. glin still treats them as missing. train_model then returns a warning that suggests the suppression_codes to confirm. Unknown codes can also stop a column from being numeric: if they make up more than 20% of the values, the column is a category. If at least half of the values are numbers, train_model returns a warning that names the codes and suggests the suppression_codes that make the column numeric. glin does not add <5 or — to the default set, because they can be real values: <5 is a value below 5, and — is often zero in financial tables. |
| Boolean columns | glin treats them as a 0/1 continuous numeric feature. |
| Dates and timestamps | glin detects them and splits them into named terms. See Dates and timestamps. |
These strings work: 2024-03-06, 2024-03-06 14:30:00, 2024-03-06T14:30:00Z, 03/06/2024, 6 Mar 2024 and Mar 6, 2024. Real datetimes work in the Python API.
EBMTabularPreprocessor(date_parse_threshold=...). glin checks dates before the identifier and cardinality rules, so a date that is unique for each row stays.<col>__year, <col>__month (jan to dec), <col>__day_of_week (mon to sun), <col>__hour and <col>__days_since_start. days_since_start counts whole days since the earliest training date. glin stores that date with the model, and inspect_model shows it.signup_date__day_of_week = sat. The other terms are ordered numbers.__year. Date-only data has no __hour. A column with one distinct date is dropped.dd/mm or mm/dd from the values. A field above 12 decides. If nothing decides, glin assumes month first, and train_model returns a warning. Use ISO YYYY-MM-DD to avoid the warning.20240306, epoch seconds and IDs stay numeric. Month-only and quarter strings (Jan, Q1 2020) and version strings (1.2.3) stay categorical. If the date matters, convert these values to date strings first.out_of_range warning. A date that does not parse returns unparseable_date. A month or weekday that training never saw returns unseen_category. A numeric date such as 05/03/2024 on a column with no numeric dates in training (ISO or month names) returns ambiguous_date_format if the day and the month can change places. glin reads it month first. A numeric date that cannot be in the training order returns date_order_mismatch, for example 02/13/2024 on a dd/mm column. glin reads it in the other order. Both warnings show the date that glin used in parsed_as. An ISO date never gets these warnings.| Data shape | What happens | Why |
|---|---|---|
Dates stored as bare numbers (year, YYYYMMDD, epoch seconds) | glin treats them as ordinary numbers, not dates. | glin cannot tell a plain integer column from an ID or a count. Convert the column to date strings if it holds dates. |
| Free text | glin drops the column if it has more than 250 unique values. Below that limit, the text becomes a set of categories that almost certainly have no use. | glin does not do natural language processing. A text column is not a set of classes. |
| Lists, dicts or nested JSON in a cell | glin does not parse them. It treats the cell as an opaque string. | No structured extraction exists. |
| Row identifiers (sequential IDs, UUIDs, hashes) | glin drops them on purpose. This is not a gap. glin checks them only above 50 rows. A numeric column is an identifier only if it is more than 95% unique, every value is a whole number, no value repeats, and one of these is true: the values fill at least 20% of the range from the minimum to the maximum (with a constant step, for example 10, 20, 30), or the column name has an ID word (id, idx, key, uuid, guid, pk, for example row_id or PassengerId, or the full name index). glin uses the same rule on a text column with mostly numeric values, for example IDs with one TBD value. glin also drops other text columns that are more than 95% unique. | IDs carry no predictive signal. |
| Any dropped column (constant, identifier-like, more than 250 categories, or a date with no varying part) | features_dropped lists the column. train_model also returns a warnings entry with the column name and the reason. For numeric identifiers, the entry suggests that you round or bin the column if it is a real measurement. | A drop is never silent. |
After preprocessing, train_model and glin train look for single feature columns that separate the target almost perfectly on their own. This is the usual sign that someone derived the target from that column with a threshold, a band or a category mapping. The model then only reads the target back.
glin flags such a column in warnings as Possible target leakage: .... The warning shows the evidence: a numeric threshold or band such as > 62.5 -> 'yes', or a category mapping such as {p, q} -> 'ok'. It suggests that you drop the column and retrain if the column is not known at prediction time. The check is only a warning. Training continues. The Python result also has leakage_suspects. The build_classifier, prepare_csv_for_training and tuning prompts tell the agent to review these warnings with the user.
n_classes + 1 leaves (one threshold, or a band with two cut points). For categorical features, the stump maps each category to its majority class. glin also uses this category map for a numeric feature with 20 or fewer distinct values, because such a column is often a category stored as integer codes (for example survey answer codes). Missing values are one more category. A target such as q12_code in {2, 5, 7} is not monotone, so a threshold cannot show it, but the category map can. glin flags a feature at balanced accuracy of 0.99 or more. For a binary target, this also means a one-vs-rest AUC of 0.99 or more. A strong but imperfect predictor does not trigger the check.missing -> .... glin checks date columns through their derived terms and reports them under the raw column, for example 'signup' (via derived term 'signup__days_since_start').prepare_csv_for_training and build_classifier prompts give the same guidance."85%" or "1" to "5". glin parses the values with the same rules as a numeric feature column. The model still uses each text value as a class.ExplainableBoostingRegressor) is planned separately. It is not yet supported.Only the Publish to PyPI GitHub Actions workflow (.github/workflows/publish.yml) publishes releases to PyPI. Nobody uploads from a local machine. Do not use twine upload, uv publish or personal API tokens. To release, push a tag of the form vX.Y.Z:
git tag v0.1.4
git push origin v0.1.4
The workflow runs the Tests workflow first (the same matrix as for pull requests). If a test fails, the workflow stops and does not publish. Then it builds the sdist and the wheel, and checks that both carry the version of the tag. Then it publishes them to PyPI as glin-ml at that exact version, and creates the GitHub Release from CHANGELOG.md.
hatch-vcs derives the package version from the git tag. pyproject.toml has no fixed version, so you do not bump the package version before a release. The plugin and registry files have their own versions (see Release steps).0.1.4.dev1+g3fa91a0. A source tree with no git metadata reports 0.0.0.glin.__version__ reads the installed package metadata.vX.Y.Z.The owner does these steps for each release:
CHANGELOG.md, move the entries under [Unreleased] to a new heading ## [X.Y.Z] - YYYY-MM-DD. Add the compare link at the bottom. Compare the entries with gh issue list --milestone vX.Y.Z --state closed.server.json (version and packages[0].version) and in plugins/glin/.claude-plugin/plugin.json (version). If the plugin needs the new release, also change the minimum version in plugins/glin/.mcp.json. Merge these changes into main.git tag v0.1.4 and git push origin v0.1.4.Publish to PyPI run and approve the pypi environment. The upload step starts only after this approval.CHANGELOG.md section of the version as the notes. If CHANGELOG.md has no section for the version, the workflow stops before it publishes.mcp-publisher login github and mcp-publisher publish. See Publishing to the MCP Registry.To compare the changelog with the merged pull requests, open a draft release on GitHub and click "Generate release notes". .github/release.yml groups the pull requests by label.
The workflow publishes through PyPI trusted publishing (OIDC). This also makes local uploads impossible.
glin-ml project → Publishing: add a trusted publisher for owner AkashChatterjee, repository glin, workflow publish.yml and environment pypi.glin-ml. Do not create new tokens. With no tokens and 2FA on, the only credential that can publish is the short-lived OIDC identity of the workflow. Also review the collaborators of the project. Remove anyone who does not need upload rights.v*. Restrict who can create tags, and block deleting or moving them. A new matching tag starts a release.pypi: add required reviewers. Every release then needs a manual approval before the upload step runs.The registry holds metadata only. It checks that the PyPI package has the line mcp-name: io.github.AkashChatterjee/glin in its README. The README has this line as a hidden comment. Publish to PyPI first, then to the registry:
version and packages[0].version in server.json are the released version.mcp-publisher login github
mcp-publisher publish
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.AkashChatterjee/glin".The registry rejects a version that already exists. Update server.json for each release.
To check a build without publishing it, run uv build locally and inspect dist/. Never upload it. After a release, confirm that the public package works with a clean install:
uv tool install glin-ml
glin train some.csv --target y --name smoke_check