Metadata-Version: 2.5
Name: tekneh
Version: 0.5.7
Summary: SIGGRAPH and SIGGRAPH Asia art-paper technē corpus — what papers state about making media art, and what they leave unstated
Project-URL: Homepage, https://tekneh.com
Project-URL: Repository, https://github.com/joonhyungbae/tekneh
Project-URL: Documentation, https://tekneh.com/about.html
Author-email: Joonhyung Bae <jh.bae@kaist.ac.kr>
License: CC-BY-4.0
Requires-Python: >=3.10
Requires-Dist: mcp>=1.2.0
Description-Content-Type: text/markdown

# Tekneh MCP

An MCP server for asking what SIGGRAPH and SIGGRAPH Asia art papers **wrote — and did not write — about implementation**.

It ships a corpus of 229 work papers (2009–2026) fully coded on 11 implementation fields.
Data is bundled in the package, so **no server and no network are required.** Tables are ~1.2MB on disk; the wheel starts instantly.

## Install — any MCP client

MCP is an open protocol. **This server is not tied to any one vendor’s product.** It speaks standard JSON-RPC over stdio, so any MCP-capable client can attach. Most use the block below as-is.

```json
{ "mcpServers": { "tekneh": { "command": "uvx", "args": ["tekneh"] } } }
```

Public source and PyPI metadata Repository: **`https://github.com/joonhyungbae/tekneh`**.
Author: Joonhyung Bae \<jh.bae@kaist.ac.kr\>. You do not need to put PyPI/GitHub in the paper body.
Export and history cleanup: `EXPORT.md`.
| Client | Where to put it | Differences |
|---|---|---|
| Claude Code | `claude mcp add tekneh -- uvx tekneh` | or `.mcp.json` |
| Claude Desktop | `claude_desktop_config.json` | block above as-is |
| Cursor | `~/.cursor/mcp.json` · `.cursor/mcp.json` | block above as-is |
| Cline · Continue · Windsurf | each extension’s MCP settings | block above as-is |
| VS Code (Copilot agent) | `.vscode/mcp.json` | top-level key is **`servers`**; entry needs `"type": "stdio"` |
| Zed | `settings.json` | top-level key is **`context_servers`** |
| Codex CLI | `~/.codex/config.toml` | **TOML** — see below |

```toml
# ~/.codex/config.toml
[mcp_servers.tekneh]
command = "uvx"
args = ["tekneh"]
```

Config schemas drift a little by client. If it does not attach, check only the key names in that tool’s latest MCP docs — `command`/`args` are the same everywhere.

### Transport

Default is **stdio**; all settings above use it. For remote or web clients:

```bash
tekneh --http --port 6230      # streamable-http, http://127.0.0.1:6230/mcp
tekneh --sse                   # legacy SSE
```

```json
{ "mcpServers": { "tekneh": { "url": "http://127.0.0.1:6230/mcp" } } }
```

From inside the repo, run `python -m tekneh_mcp`. Data location: `TEKNEH_DATA=/path` or `--data`.

### Reading it yourself

`tekneh` run bare in a terminal prints this rather than waiting on stdin — it is a server, and a
terminal is not a client. The same queries also run there, rendered for a person:

```bash
tekneh info                                  # what this corpus is and is not
tekneh vocab                                 # every value the filters accept
tekneh stats venue_requirements --form Sound
tekneh profile "Cacophonic Choir"
tekneh intent "reacts when a person approaches"
tekneh --version
```

The reading contract is set in the same weight as the content, not as a footnote: the denominator
sits beside the number, a blocked claim gets a red gutter, a permitted checklist reading gets a
green one, and the corpus boundary closes every response. `--format json` for the raw object
(the default when stdout is not a terminal), `--color never` to drop the escapes.

### Language

`TEKNEH_LANG=ko` — or `tekneh --lang ko` in the server config — sets the default for every tool and
every CLI call, so a Korean session does not repeat `lang` twelve times. An explicit `lang` on a
call still wins.

```json
{ "mcpServers": { "tekneh": { "command": "uvx", "args": ["tekneh"],
                              "env": { "TEKNEH_LANG": "ko" } } } }
```

### If the tool does not use MCP

The same queries are available via the MCP tools above (or `uvx tekneh` with an MCP client).
Return shape matches the tool results. Codebook role labels such as `런타임` (runtime) are stored as-is in the CSVs.

## Language

Every tool takes `lang`: **`en`** (default) or **`ko`**. Match the user’s language so boundary, absent phrasing, warnings, and form/role labels come back in that language. Filters accept Korean codebook labels **or** English glosses (`Sound`, `Runtime`, `Installation`, …). Tool names are alias-normalized at query time (`Unity3D` → `Unity`).

## Reading contract (in every success response)

Prefer these fields over free paraphrase:
- **`report_value` / `report_kind`** — cite this number (`human_verified` · `lower_bound` · `machine`). When human-verified, do not cite `rate`.
- **`generalization_allowed`** — if false, do not generalize; always keep the denominator.
- **`must_say` / `forbidden_paraphrase`** — include boundary / absent phrasing; never say “did not have”.
- **`claim_template` / `citation_ready_sentence`** — ready-to-use sentence with boundary and n.
- **`checklist_allowed`** — present only on a small cell with a real denominator. It permits reading
  that one cell as a preparation prompt for a specific piece of work. It never licenses a rate,
  a percentage, or a fraction. Absent everywhere else.
- **`detail_language` / `detail_source` / `detail_quotable`** — `detail` is a 2026 coder's note,
  written in Korean and machine-translated for `lang=en`. The translation can carry pronouns and
  tense the paper never used. Never attribute it to the authors. Quote `quote`, which is capped
  at 20 of the paper's own words; `citation_ready_scope` says so.
- **`match_layer` / `tag_layer`** — `text_match` is unanchored substring matching over coder
  vocabulary and comes with `generalization_allowed: false`; `machine_unverified` marks the tag
  layer, which has no human-verified tier at all.
- **`citation_status`** — `ready` (705 quotes) means the span was confirmed verbatim against the
  paper. `verify_first` (961) means it was aligned to the closest span of the extracted text or
  could not be checked: the locator is good, the string is not quotable as it stands. `blocked`
  means a visible extraction artifact. Only `ready` sets `citation_ready`. Two-column PDFs
  interleave the neighbouring column often enough that an unconfirmed span is not worth
  reproducing — open the doi and copy the sentence from the paper.
- **`quote_verbatim`** — the provenance behind that: `exact`, `repaired`, `unverified`. Line-break
  hyphens are rejoined for display (`quote_repairs`); a repair improves the reading, never the
  citation status.
- **`asks`** — one line saying what the work is about, on every work row. It is the coded `concept`
  field, which the codebook defines as the work's question compressed to 40 words, machine-translated
  for `lang=en`. It tells two works apart; it is not the paper's sentence and is not quotable.
- **`url` / `acm_url`** — `https://doi.org/…` and the ACM DL address, on every work row. Open them
  in a browser; ACM DL blocks scripted access, so `link_note` says not to fetch them.

## Tools

| Tool | What it does |
|---|---|
| `corpus_info` | Corpus boundary · population · human-confirmed values · citation rules. **Call this first** |
| `list_vocab` | Every value the filters accept: forms · roles · venues · fields · **tags** · top tools · years, plus `not_covered`. **Call before guessing a filter**. `detail="full"` adds per-tag counts and the rest |
| `field_stats` | How often a field is stated. Denominator and absent count together |
| `search_works` | Search works. **`absent` filter**, `tag` (material level, field-qualified), `q_in=title\|text\|all`; every row carries `asks` and a `url` |
| `work_profile` | **One work, whole**: 11 fields + tools with roles + an input→sensing→computation→output chain. Use instead of a `cite` per field |
| `works_using` | Works that used a tool (aliases resolved). `role` = runtime / production / training |
| `cite` | Source quote (20 words) · section locus · doi |
| `absent_to_questions` | High-absent fields for a **form** → finished questions. `audience=artist` (check your own work) or `interviewer` (ask an artist) |
| `map_intent` | Closed router: short phrase → field (no SQL). Takes **the language of experience** (“reacts when a person approaches”) and returns `experience_route`. `not_covered` when the corpus cannot answer that axis at all |
| `claim_pack` | Academic bundle: stats + boundary + stated/absent cite examples |
| `year_trend` | Per-year rates with `trend_narratable` guard (no smoothing) |
| `compare_fields` | Two-field cross-tab with per-cell contracts |

## If you make the work rather than study it

```
map_intent("사람이 다가가면 반응하는")            → experience_route: tags to search with
search_works(tag="sensing:근접·거리")            → the works
work_profile("SwarmVision")                    → one work, whole: 11 fields + tools + chain
absent_to_questions(form="Sound", audience="artist")
list_vocab()                                   → forms, tags, tools, and what is not here
```

You do not have to know the codebook to start. `map_intent` takes what you saw — *“만지면 소리가
나는”*, *“a robot that moves when you look at it”* — and hands back field-qualified tags to search
with, because the useful answer is works to read, not the name of a field. `work_profile` then
gives you one of them whole, in a single call: every field with its quote and locator, the tools
with their roles, and one line reading input → sensing → computation → output.

`audience="artist"` rephrases the gaps as prompts for your own work and text —
*“Do you have a one-page technical rider?”* rather than *“What conditions did the venue need?”*
It stays `normative: false`. This corpus records what 229 papers stated; it holds no data on what
reviewers, curators or interviewers ask, and it does not predict that. Which of these gaps matter
for your work is your call.

Three things to know going in. `tag` must name its field — `sensing:Depth & LiDAR`,
`computation:Microcontroller` — because `Other` alone exists in five fields and unioning them
gives a denominator that means nothing. The `tool` filter is **software only**: Kinect and
Raspberry Pi are in the tags and notes, and `works_using` returns `text_route` / `tag_route`
instead of a bare zero. And `q_in="text"` finds work near your own subject by substring, over
coder notes rather than the papers — good for finding things to read, useless for counting them.

## Response size

`list_vocab` and `search_works` are the two calls a client makes without knowing what it will get
back, so both are sized for a first call: `list_vocab` defaults to a summary (~1.9k tokens against
3.5k for `detail="full"`), and `search_works` / `works_using` return 15 rows unless you ask for
more (~2.7k tokens against 8.5k at 50). `limit` is capped at 100 and says so when it clamps. If you
want the number and nothing else, `field_stats` is ~270 tokens where `claim_pack` is ~1.7k.

## Four reading rules

This corpus is valuable because **you can count what is missing**. Reading it differs from the usual.

1. **`absent` is a property of the document, not the work.** Not “this work used no power,” but
   **“not stated in the paper (whether it existed is unknown).”** Use the return’s `phrasing` and `reading_note` as written.
2. **If `generalization_allowed` is false (or `small_cell` is true), do not generalize.** Denominator under 30. Sound has 27 papers, robotics 30, so
   “sound is done with Max/MSP” (4/27) is false. Always report the denominator.
   A small cell may also carry `checklist_allowed` — permission to ask *“has my own work got a
   technical rider?”*, not permission to restate the cell as a proportion. A fraction is a rate.
3. **State `boundary` in the answer.** SIGGRAPH and SIGGRAPH Asia art papers 2009–2026 only.
   No Ars Electronica, NIME, ISEA, exhibition catalogs, or artist websites. The population is **papers, not works**.
4. **Prefer `report_value` / `use_this` over `rate`.** The machine overcounts prose lists and third-party URLs as stated.

| Field | Machine | Value to use |
|---|---:|---|
| Venue requirements | 25.9% | **20.9%** (16.6–25.3) |
| Reproducibility (public) | 10.5% | **6.6%** — a **lower bound**, not a point estimate. Write “7–14%” |

Human-verified values come from a targeted audit combined with a lexical screen, three strata,
229 papers. The stratum most likely to hide a false `absent` — machine-`absent` papers that the
screen flagged — was read in full (23/23 and 26/26), so it carries no sampling error. These are
the values the manuscript reports; the earlier audit-only pair (20.5% / 6.1%) is kept alongside
as `human_verified.*.audit_only`.

## What this corpus does well / poorly

**Does well.** What it was made with — tools named in ~80% of papers; 428 distinct kinds (alias-normalized) with
runtime / production / learning roles. Form × tool joins are available via `search_works` / `compare_fields`.

**Does poorly.** How to remake it. Venue conditions are stated in 20.9%; public release in only 7–14%.
Turning `absent` into **questions** — to ask an artist, or to check your own work against — is the proper use of this data.

**Does not hold at all.** Budget · shipping and insurance · circuit debugging · sales.
`map_intent` returns these as `not_covered` with an `ask_instead` rather than answering from a
nearby field. Verified against the coded table: no production cost, no wiring faults, no freight.

**Holds only by accident.** Schedule · team size · long-run maintenance come back as
`partially_covered` with the fields they hide in — 82 of 177 collaboration notes happen to carry a
headcount, a few process notes give a duration, one work records a four-month run with a technician
on standby. There is no field for any of it, so you can read them and you cannot count them.

## Citation

Annotations are CC BY 4.0. **Source quotes are capped at 20 words per field**; the server enforces that bound.
For more, follow `doi` to the original paper — ACM DL blocks scripted access, so do not crawl.
Roughly 40% of quotes carry `citation_status: ready`; the rest give you the locator and expect you
to read the sentence in the paper.

## Building from the repository

Shipped wheels already include `query.py` and the CSV tables under `src/tekneh_mcp/`.
Those files are produced before `uv build` and are not committed.

```bash
bash sync.sh     # maintainers: refresh bundled data, then:
uv build
UV_PUBLISH_TOKEN=pypi-... bash publish.sh --upload
```

Or use GitHub Actions Trusted Publishing so Actions uploads without a token — often requires a public repo.

See `EXPORT.md` for packaging notes.
