
Thingctx
com.thingctx由 thingctx.com 發布v0.2.2更新於 Oct 24, 2026
Drive devices and APIs from their W3C Thing Description. You gate every operation.
概覽
通用 MCP 橋接器,把 W3C 物描述檔轉換成可呼叫的工具,用來驅動 API、裝置與本機程序,並具備逐操作審批與策略閘門。
- 可以做什麼
- 把資料夾、URL 或物描述目錄中的每個動作、屬性與事件公開為名為 thing__action 的工具
- 依每個 form 指定的傳輸(HTTP、MQTT、Redis、local、exec)呼叫動作,讀取、寫入或訂閱屬性
- 透過審批閘門確認或擱置有風險的呼叫,並用 approve 工具在對話中確認待處理的呼叫
- 把 OpenAPI 3.x 規格匯入為描述,並在命令列列出或呼叫工具
- 為已執行或被拒絕的操作產生 Observation、Actuation 與 Refusal 記錄
- 適用情境
- 當你的助理宿主只支援 MCP,但你希望它驅動以 W3C 物描述寫好的既有 REST API、裝置或本機程序,而不必為每個整合另外寫 MCP 伺服器時使用。也適合需要在真實系統前加上人工審批或唯讀策略的情境。
- 執行需求
- 以 stdio 在本機執行;安裝 mcp 與 http 附加元件後,用 thingctx-mcp 指令指向描述資料夾(或 URL、物描述目錄)啟動。密鑰從名為 THINGCTX_TOKEN_ 的環境變數讀取;THINGCTX_APPROVE_WHEN、THINGCTX_POLICY 與 THINGCTX_FS_ROOT 分別設定審批策略、唯讀模式與檔案系統沙箱。MQTT、Redis 等選用傳輸需另裝附加元件。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Thingctx,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
thingctx
[CI] [PyPI] [Python] [License] [W3C WoT]
[thingctx gating and routing a tool call]
thingctx turns a description of any API, device, or local process into tools an AI agent can call: authorized per operation, over the system's own transport, and the agent never holds your keys. The description is a W3C Web of Things Thing Description (a TD), a plain JSON file naming what the system can do. Write one, or compile it from an OpenAPI spec. The described system is the server, so you run no server per integration.
A "Thing" is whatever you want to describe: a REST API, an app, a sensor, a
media server, a local object, or one system that is several of those at
once. A media server takes commands over HTTP and serves its stream over
RTSP. The pump in examples/ reads over MQTT and acts through
a local call. Each is one Thing and one description.
A description names actions, properties, and events, and gives each one
a form: the entry that says where and how to reach it. The URL scheme in that
address picks the transport per call, so however many protocols one Thing
spans, the agent still sees one tool set.
docs/BINDINGS.md covers the transports and how to add
your own. A subprocess transport, exec, also ships, and it ships locked:
it refuses every command until you hand it an explicit allowlist.
Those three stay distinct all the way to the call. A read, a write, and a subscribe do not collapse into one undifferentiated function, which is why the gate can allow the read and refuse the write on the same system. A flat list of functions has no such handle.
Pick your path: use it as a library if you own the agent loop, the command line if you do not want to write code, the MCP bridge if your host only speaks MCP, or add a transport if thingctx does not speak your protocol yet.
Stuck, or wondering whether something is possible? Ask in Discussions. Want to build something small? The open issues say what would help.
First run: no keys, no network
One paste shows the whole idea. The description is inline and the handler ships with the package, so nothing here needs a key, a network, or a second file:
That is the whole loop: a description in, tools out, calls routed.
LocalBinding is a binding, a transport implementation; this one routes the
local:// address to the handler you pass it. Every other transport works
the same way; only the form's href changes.
Install
Quote the argument; unquoted brackets fail in zsh. Base
pip install thingctx has no dependencies; it already includes the local
and exec transports. Redis support is pip install 'thingctx[redis]'. Every optional
transport and capability has an extra,
listed in pyproject.toml; each one this page uses is
named next to the code that needs it.
The document
A description can be small. This one drives the live, key free
Open-Meteo forecast API, so it runs as written.
Save it as weather.td.json, a later example uses it:
thingctx projects each action to a tool named <thing>__<action>: this file
yields weather__forecast (a version segment on the id is dropped).
docs/MAPPING.md specifies the full projection. The
description carries no secrets, so it is safe to commit and share. The
secret is supplied at runtime to the binding that carries the call. Secrets
are keyed per Thing.
If your system already has an OpenAPI 3.x spec, you do not have to author a description at all:
The spec can be a file or a URL, JSON or YAML; --base-url, --id, and
--title override what the spec says. The result is a normal description,
so everything else here applies to it unchanged.
Ready made descriptions for common apps, developer tools, and devices live at td.thingctx.com.
Use it as a library
Own the agent loop? Read descriptions, hand the specs to your model, and
route each call back through invoke. ThingClient needs no LLM and has no
opinion on what chose the action; any code can drive it. This sketch shows
the surface against a folder describing a pump; from_arg turns a path or
URL into a registry of descriptions:
The form picks the transport per call, so one client can read over HTTP, subscribe
over MQTT, and read or write cached properties over Redis. Bindings that pull optional
dependencies, including MQTT and Redis, are off by default: install the extra and pass
bindings=thingctx.BindingRegistry.default(mqtt=True, redis=True).
Want the loop handled for you? The llm extra runs any provider through
litellm; add the http extra, since the weather file's form is HTTPS. Set
your provider key in its usual variable (OPENAI_API_KEY,
ANTHROPIC_API_KEY, and so on) and pick a model with THINGCTX_MODEL:
The model calls weather__forecast itself and answers in prose:
Safety: approval and authorization
Two opt in layers stand between an agent and a real system; both run before any transport is selected. Neither layer handles a credential. The binding holds the secret and the model never sees it.
Approval gates risky calls behind a human. Risk is read from the
description, and when to gate is yours: declared (only actions the
description marks risky; the default), destructive (adds non idempotent
actions and every property write), all, or never. A gated call with no
approver is denied: a gate with nobody to open it stays shut. The check sits
inside invoke and write_property, so it covers the LLM loop, direct
callers, and the MCP bridge alike.
Carrying on from the first example above, with the same TD:
Authorization decides from policy, per caller, per operation. Pass a
pdp and an identity and every call authorizes the resolved
(thing, affordance, operation) before it reaches the system; an affordance
is an action, property, or event named in the description. This snippet condenses
examples/14_authz.py, which runs as is on the core
install. It defines the TD and Pump used below: a pump with one
property, target_rpm.
Alice's operator role grants read on target_rpm and nothing else, so her
reads pass and her writes are refused. The vocabulary is closed: a grant is
honored only for operations the description declares. thingctx.authz
imports no crypto and runs on the dependency free core. Identity is claims
that something upstream validated, never the credential itself.
Running the example prints:
The full model is in docs/SECURITY.md.
The MCP bridge
Some agents only take tools over MCP. For those, thingctx ships one generic MCP server, with no per integration server. It serves any registry of descriptions: a folder, a URL, or a W3C Thing Description Directory. A Directory is the standard catalog server for descriptions.
For Claude Desktop, add this to
~/Library/Application Support/Claude/claude_desktop_config.json
(on Windows: %APPDATA%\Claude\claude_desktop_config.json) and restart:
The same approval gate applies here: the bridge marks risky tools and asks
the client to confirm. Declining denies the call. A client that cannot show
a confirmation dialog gets the call parked as pending instead, and the
bridge's approve tool confirms it from the chat.
THINGCTX_APPROVE_WHEN picks the policy, and THINGCTX_POLICY set to
read-only denies writes and state changing actions outright.
Descriptions never carry secrets, so the bridge reads them from the
environment: THINGCTX_TOKEN_<SLUG> binds a secret to the Thing with that
slug (THINGCTX_TOKEN_GITHUB serves github), applied per the Thing's
declared scheme.
The command line
The thingctx command drives the same runtime from a terminal. Point list
and invoke at any folder of descriptions. Save the TD from the first run
above as things/time.td.json, then:
That works with no extra wiring because the clock's handler ships with the
package and binds to the urn:thingctx:time id in the description.
list prints every tool the folder's Things expose, with its schema;
invoke runs one action. thingctx --help lists the other subcommands,
including the OpenAPI importer shown above. invoke honors the same
approval gate. --approve-when picks the policy (THINGCTX_APPROVE_WHEN
is the fallback), -y approves without a prompt, and with no terminal and
no -y a gated call is denied with a non zero exit. An agent shelling out
gets the same protection as one holding the library.
Records: what a gate writes down
When thingctx reads a sensor, sends a command, or refuses one, it can write a record of that. There are three kinds:
Every field in those records uses a term from a published vocabulary. SOSA, the W3C and OGC sensor ontology, defines what an observation and an actuation are. Four others fill the gaps SOSA leaves: PROV-O for who asked, schema.org for how a command ended, DCAT for where the raw data is, and Dublin Core for the reason the gate gave. thingctx invents no term of its own. A test resolves every term against the file its publisher serves and fails if one is missing.
Turn it on with one argument. The client then reports every read, write, command,
query and cancel it runs, and every one a gate refuses, as an Operation: who asked,
what, the gate's answer, what came back, and when. A record model turns that into its
records. For SOSA:
A long running action is one Actuation: its start writes the record without an
outcome, and the query or cancel that ends it writes the same record with the outcome.
Operation lives in thingctx.operations, imports nothing, and is the only thing the
runtime and a record model share.
The record classes are generated, not written
The three classes come from one file,
records.yaml, written in
LinkML. That file lists each field, its type, the vocabulary term
it maps to, and which fields make two records the same record. Run the generator and
it writes the Python classes, the vocabulary tables, and a JSON Schema:
Here is the directory. You read three files and never touch _generated:
records.py and records.schema.json are exactly what LinkML's own commands write.
You can run gen-pydantic or gen-json-schema by hand and get the same bytes; a test
checks that. terms.py comes from thingctx's own script instead, because LinkML's RDF
generators (gen-owl, gen-jsonld-context) make up a new term for every class and
field, and thingctx wants only the published ones.
_base.py holds the behaviour a vocabulary cannot describe: a record refuses a
timestamp with no timezone, derives its id from its own content so the same fact
written twice is one row, and knows how to turn itself into a table row or a JSON-LD
document. It mentions no vocabulary term. Each generated class inherits it.
To add a model for another vocabulary, create src/thingctx/models/<name>/, put
a records.yaml in it, run the generator, and add a recording.py with the same two
functions SOSA has, records_for(operation) and recorder(write). The generator,
_base.py and the runtime need no change.
Bindings never see vocabulary
A protocol binding moves bytes over HTTP, MQTT or a rosbridge WebSocket. It does not
know what a payload means. The records above sit in a layer the bindings cannot
import. import-linter contracts in pyproject.toml enforce that, so a binding that
imports a vocabulary fails the build. Driving a Thing never loads the record model.
Extend it
Describing a new system needs nothing from this repo. Teaching thingctx something it does not know yet is one class, and every part works the same way, so what you learn once applies to the rest.
A transport is the common case:
The contract is a scheme plus an async invoke. Add read, write, or
subscribe if the transport supports them; the runtime checks which methods
exist before calling, so a pub sub transport without reads simply has none.
Pass the binding to the client and it is live. Registering one for a scheme
thingctx already serves replaces the built in binding.
The same shape covers the rest:
Each contract is a typing.Protocol: match the methods, inherit nothing.
@implements(Contract) checks the match at import time, and the conformance
kit in thingctx.testing checks the runtime behaviour a type checker cannot.
examples/13_custom_stack.py builds one of
each in a single file, offline.
Keep what you write private in your own package, or contribute it so nobody writes it twice. Out of tree packages register through entry points and are discovered without a fork, so installing yours is all it takes:
See docs/BINDINGS.md.
Interoperability
thingctx consumes a description no matter who produced it. Demos under
examples/interop/ prove it end to end: Eclipse
Thingweb's node-wot serves a Thing and
thingctx drives it; Eclipse
Ditto generates a description for a digital twin
and thingctx round trips state through it. The Thing Description is a W3C
Recommendation, so a file you write here is portable to any other WoT
consumer, and thingctx reads a W3C Thing Description Directory.
Contributing
Driving your own system needs nothing from this repo: write a description and point thingctx at it. What the project does need is transports it cannot speak yet, and each one is a single class.
Good places to start:
- good first issue is scoped work with the context written down. Comment to claim one and it gets assigned to you.
- help wanted is everything else that is open.
- CONTRIBUTING.md explains how a binding fits together.
Contributor patches have added a lint rule and closed a coverage gap. Small, scoped, merged.
Every commit needs a DCO Signed-off-by line, which git commit -s adds. AI
assisted patches are welcome; a real human reviews and signs off.
Questions do not need an issue. Discussions is the place for "does this already do X", "would you take a PR for Y", and how to reach a system you have. Asking there is useful even when the answer is no, because it tells the project what people are trying to do.
License
Apache 2.0. See LICENSE.
來源:README.md,提交 102f858
工具
0版本歷史
1- v0.2.2最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分