mcpField
T
thingctx

thingctx

本地服务
thingctx.com开发工具162 天前更新

关于 thingctx

thingctx 将 W3C Thing Description 转换为 API、设备和本地进程可调用的工具,并提供库、命令行和 MCP 桥接方式。它区分读取、写入和订阅操作,并可在调用到达系统前按操作进行审批与授权。

主要功能

  1. 01将 Thing Description 映射为工具
  2. 02导入 OpenAPI 3.x 规范
  3. 03通过多种传输路由调用
  4. 04通过审批和授权控制操作
  5. 05记录观测、执行和拒绝

适用场景

  1. 01向 Agent 提供描述过的 API 或设备
  2. 02允许读取并限制设备写入
  3. 03向 MCP 客户端提供描述注册表

MCP 相关

通用 MCP 桥接可从 W3C Thing Description 注册表提供工具,注册表可以是文件夹、URL 或 Thing Description Directory。它与库和命令行使用相同的审批门控。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"com-thingctx-thingctx": {
"command": "uvx",
"args": [
"thingctx"
],
"env": {
"THINGCTX_POLICY": "<THINGCTX_POLICY>",
"THINGCTX_APPROVE_WHEN": "<THINGCTX_APPROVE_WHEN>",
"THINGCTX_FS_ROOT": "<THINGCTX_FS_ROOT>"
}
}
}
}
启动命令uvx thingctx
需要配置的环境变量
THINGCTX_POLICYWhat the agent may do: read-only | full.
THINGCTX_APPROVE_WHENWhen the agent asks before acting: declared | destructive | all | never.
THINGCTX_FS_ROOTSandbox directory for the filesystem Thing. Unset = refuse every filesystem call.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypithingctx0.2.2

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

thingctx

CI PyPI Python License W3C WoT

thingctx 对工具调用进行授权和路由

thingctx 可将任意 API、设备或本地进程的描述转换为 AI Agent 可调用的工具:逐项授权,通过系统自身的传输方式调用,并且 Agent 永远不会持有你的密钥。 该描述是 W3C Web of Things Thing Description(简称 TD),一个列出系统功能的普通 JSON 文件。你可以编写一个,也可以从 OpenAPI 规范编译生成。被描述的系统本身就是服务,因此每种集成无需单独运行服务。

“Thing” 是你想描述的任何事物:REST API、应用、传感器、媒体服务、本地对象,或同时具备其中多种特征的系统。媒体服务通过 HTTP 接收命令,并通过 RTSP 提供流。 examples/ 中的水泵通过 MQTT 读取,并通过本地调用执行操作。每个都是一个 Thing,也对应一个描述。

描述会列出 actions、properties 和 events,并为每项提供一个 form:说明如何以及从何处访问该项的入口。该地址中的 URL scheme 决定每次调用所用的传输方式,因此,无论一个 Thing 涉及多少种协议,Agent 看到的始终是一组工具。

docs/BINDINGS.md 介绍了各种传输方式以及如何添加自己的传输方式。项目还提供子进程传输方式 exec,并默认锁定:在你明确提供允许列表之前,它会拒绝所有命令。

这三类操作从始至终都各自独立。读取、写入和订阅不会合并成一个无法区分的函数,因此授权关卡可以允许读取,却拒绝对同一系统的写入。扁平的函数列表无法做到这一点。

选择适合你的路径:将其用作库,适合自行管理 Agent 循环的情况;命令行,适合不想编写代码的情况;MCP 桥接,适合宿主只支持 MCP 的情况;或者添加传输方式,适合 thingctx 尚不支持你的协议的情况。

遇到问题,或想知道某件事是否可行?欢迎在 Discussions 中提问。想做点小贡献?未解决的问题列出了可以帮上忙的事项。

首次运行:无需密钥,无需网络

只需粘贴一次,即可了解整体思路。描述内联编写,处理程序随软件包一同提供,因此这里无需密钥、网络或第二个文件:

pip install thingctx
import asyncio

import thingctx
from thingctx.contrib.time import make_time_handler

TD = {
    "@context": "https://www.w3.org/2022/wot/td/v1.1",
    "id": "urn:thingctx:time",
    "title": "Time",
    "securityDefinitions": {"nosec_sc": {"scheme": "nosec"}},
    "security": ["nosec_sc"],
    "actions": {
        "getCurrentTime": {
            "description": "Current time in an IANA timezone (default UTC).",
            "input": {
                "type": "object",
                "properties": {"timezone": {"type": "string"}},
            },
            "safe": True,
            "idempotent": True,
            "forms": [{"href": "local://getCurrentTime"}],
        }
    },
}


async def main():
    client = thingctx.ThingClient(tds=[TD], bindings=[thingctx.LocalBinding(make_time_handler())])
    tools, invoke = client.as_tools()  # specs for your model; invoke runs a call
    print("tools:", [t["function"]["name"] for t in tools])
    print(await invoke("time__getCurrentTime", {"timezone": "UTC"}))


asyncio.run(main())
tools: ['time__getCurrentTime']
{'timezone': 'UTC', 'datetime': '2026-07-26T15:59:03.233080+00:00', 'utc_offset': '+0000'}

这就是完整的流程:输入描述,生成工具,路由调用。LocalBinding 是一种绑定,也就是传输实现;它会将 local:// 地址路由到你传入的处理程序。其他传输方式的工作原理相同,只有 form 的 href 会变化。

安装

pip install 'thingctx[llm,http,validate]'

请将参数加引号;在 zsh 中不加引号的方括号会导致失败。基础安装 pip install thingctx 不含依赖;它已包含 local 和 exec 传输方式。Redis 支持可通过 pip install 'thingctx[redis]' 安装。每种可选传输方式和功能都对应一个 extra,详见 pyproject.toml;本文用到的每个 extra 都会标注在所需代码旁边。

该文档

描述可以很精简。这个描述驱动的是实时、无需密钥的 Open-Meteo 预报 API,因此可以直接按原样运行。 将它保存为 weather.td.json,后面的示例会用到它:

{
  "@context": "https://www.w3.org/2022/wot/td/v1.1",
  "id": "urn:example:weather:v1",
  "title": "Weather",
  "securityDefinitions": { "nosec_sc": { "scheme": "nosec" } },
  "security": ["nosec_sc"],
  "actions": {
    "forecast": {
      "input": {
        "type": "object",
        "properties": {
          "latitude": { "type": "number" },
          "longitude": { "type": "number" },
          "current": { "type": "string" }
        },
        "required": ["latitude", "longitude"]
      },
      "forms": [{ "href": "https://api.open-meteo.com/v1/forecast", "htv:methodName": "GET" }]
    }
  }
}

thingctx 会将每个操作映射为名为 <thing>__<action> 的工具:此文件会生成 weather__forecast(ID 中的版本段会被去掉)。 docs/MAPPING.md 详细说明了完整的映射规则。该描述不包含任何密钥,因此可以安全地提交和分享。密钥会在运行时提供给承载调用的绑定。密钥按 Thing 单独设置。

如果你的系统已有 OpenAPI 3.x 规范,就无需编写描述:

pip install 'thingctx[openapi]'
thingctx import openapi https://api.example.com/openapi.json --out api.td.json

该规范可以是文件或 URL,格式可以是 JSON 或 YAML;--base-url、--id 和 --title 可覆盖规范中的相应内容。生成的结果是普通描述,因此这里的其他内容对它同样适用。

常见应用、开发工具和设备的现成描述可在 td.thingctx.com 找到。

将其用作库

自己编写 Agent 循环?读取描述,将规范交给模型,再通过 invoke 路由每个调用。ThingClient 无需大模型,也不限制谁来选择操作;任何代码都可以驱动它。下面的示例展示了如何使用一个描述水泵的文件夹;from_arg 可将路径或 URL 转换为描述注册表:

import thingctx


async def run():  # a sketch, not a program: call it from your own loop
    client = thingctx.ThingClient.from_registry(thingctx.from_arg("./descriptions/"))
    specs, invoke = client.as_tools()

    await invoke("pump__set_speed", {"rpm": 1500})
    await client.read_property("pump__rpm")
    await client.write_property("pump__target_rpm", 1500)  # gated like invoke
    async for evt in await client.subscribe(
        "pump__overheat"
    ):  # subscribe returns an async iterator
        ...

表单会为每次调用选择传输方式,因此一个客户端可以通过 HTTP 读取、通过 MQTT 订阅,并通过 Redis 读取或写入缓存属性。需要拉取可选依赖的绑定(包括 MQTT 和 Redis)默认处于关闭状态:安装相应额外依赖,并传入 bindings=thingctx.BindingRegistry.default(mqtt=True, redis=True)。

想让程序替你处理 Agent 循环?llm extra 可通过 litellm 使用任意提供商;由于天气文件中的表单使用 HTTPS,还需添加 http extra。将提供商密钥设置到其惯用的变量中(OPENAI_API_KEY、 ANTHROPIC_API_KEY 等),并使用 THINGCTX_MODEL 选择模型:

import asyncio
import thingctx


async def main():
    # weather.td.json from above; it points at a live, key free API
    host = thingctx.from_file("weather.td.json")
    print(await host.chat("What is the forecast for Cairo? Latitude 30.0, longitude 31.2."))


asyncio.run(main())

模型会自行调用 weather__forecast,并以自然语言作答:

The forecast for Cairo is clear, around 34 C.

安全:审批与授权

在 Agent 与真实系统之间,有两个可选启用的层;它们都会在选择任何传输方式之前运行。这两个层都不处理凭据。密钥由绑定持有,模型永远不会看到它。

审批会要求人工批准有风险的调用。风险取自描述,是否需要审批由你决定:declared(仅审批描述中标记为有风险的操作;默认值)、destructive(增加非幂等操作和所有属性写入)、all 或 never。如果受审批的调用没有审批者,则会被拒绝:无人开启的审批门会一直关闭。检查位于 invoke 和 write_property 内部,因此 LLM 循环、直接调用者和 MCP 桥接都受其保护。

沿用上面的第一个示例,使用相同的 TD:

def approve(req):  # sync or async; return True to allow
    return input(f"run {req.tool_name}{req.arguments}? [y/N] ").lower() == "y"


client = thingctx.ThingClient(
    tds=[TD],
    bindings=[thingctx.LocalBinding(make_time_handler())],
    approve=approve,
    approve_when="all",
)

授权根据策略,按调用者和操作分别作出决定。传入 pdp 和 identity 后,每次调用都会先针对已解析的 (thing, affordance, operation) 进行授权,然后才会到达系统;affordance 是描述中命名的操作、属性或事件。下面的代码片段精简自 examples/14_authz.py,只需安装核心依赖即可直接运行。它定义了下文使用的 TD 和 Pump:一个带有 target_rpm 属性的水泵。

from thingctx import LocalBinding, ThingClient
from thingctx.authz import LocalPolicyGrantSource, PolicyDecisionPoint, build_vocabulary

reader = ThingClient(
    tds=[TD], bindings=[LocalBinding(Pump())]
)  # no pdp yet; parsed only to read the description
vocab = build_vocabulary(reader.things)  # the closed set the description declares

grants = LocalPolicyGrantSource({"operator": {("urn:demo:pump", "target_rpm", "readproperty")}})
pdp = PolicyDecisionPoint(vocabulary=vocab, grant_source=grants)
identity = {"sub": "alice", "roles": ["operator"]}  # claims, validated upstream

client = ThingClient(tds=[TD], bindings=[LocalBinding(Pump())], pdp=pdp, identity=identity)

Alice 的 operator 角色只授予对 target_rpm 的读取权限,因此她的读取操作会通过,写入操作则会被拒绝。可用词汇是封闭的:只有描述中声明的操作才会获得授权。thingctx.authz 不导入加密依赖,并可在无需依赖的核心版本上运行。身份是上游已验证的声明,而不是凭据本身。

运行该示例会输出:

vocabulary (grantable tuples the TD declares):
  (urn:demo:pump, target_rpm, readproperty)
  (urn:demo:pump, target_rpm, writeproperty)

READ  target_rpm  -> ALLOWED, device returned 1200
WRITE target_rpm  -> DENIED, grant does not include ('urn:demo:pump', 'target_rpm', 'writeproperty')

RE-READ target_rpm -> 1200  (unchanged: the denied write never ran)

完整模型见 docs/SECURITY.md。

MCP 桥接器

有些 Agent 只通过 MCP 使用工具。为此,thingctx 提供一个通用 MCP 服务,不为每个集成单独提供服务。它可以提供任何描述注册表:文件夹、URL 或 W3C Thing Description Directory。Directory 是用于描述的标准目录服务。

pip install 'thingctx[mcp,http]'
thingctx-mcp ./descriptions/

对于 Claude Desktop,将以下内容添加到 ~/Library/Application Support/Claude/claude_desktop_config.json (Windows 上为 %APPDATA%\Claude\claude_desktop_config.json),然后重启:

{ "mcpServers": { "things": {
  "command": "thingctx-mcp",
  "args": ["/path/to/your/descriptions/"],
  "env": { "THINGCTX_APPROVE_WHEN": "destructive" } } } }

这里同样适用审批关卡:桥接器会标记有风险的工具,并要求客户端确认。拒绝即会拒绝调用。如果客户端无法显示确认对话框,调用会改为挂起等待处理,桥接器的 approve 工具可在聊天中确认该调用。 THINGCTX_APPROVE_WHEN 用于选择策略,而将 THINGCTX_POLICY 设为 read-only 则会直接拒绝写入和更改状态的操作。

描述中不会包含机密,因此桥接器会从环境变量中读取机密:THINGCTX_TOKEN_<SLUG> 会将机密绑定到具有相应 slug 的 Thing(THINGCTX_TOKEN_GITHUB 用于 github),并按照 Thing 声明的方案应用。

命令行

thingctx 命令可在终端中驱动同一个运行时。将 list 和 invoke 指向任意包含描述的文件夹。将上文首次运行时生成的 TD 保存为 things/time.td.json,然后:

thingctx list ./things/
thingctx invoke ./things/ time__getCurrentTime --arg timezone=UTC
{
  "timezone": "UTC",
  "datetime": "2026-07-26T16:45:57.114817+00:00",
  "utc_offset": "+0000"
}

无需额外配置即可运行,因为时钟的处理程序随软件包提供,并绑定到描述中的 urn:thingctx:time ID。

list 会列出文件夹中各个 Thing 提供的所有工具及其架构; invoke 会运行一个操作。thingctx --help 会列出其他子命令,包括上文展示的 OpenAPI 导入器。invoke 遵循相同的审批关卡。--approve-when 用于选择策略(THINGCTX_APPROVE_WHEN 为回退值),-y 表示无需提示即可批准;如果没有终端且未指定 -y,则会拒绝受审批关卡保护的调用,并返回非零退出码。通过命令行调用的 Agent 获得的保护,与使用该库的 Agent 相同。

记录:审批关卡会记录什么

thingctx 读取传感器、发送命令或拒绝命令时,可以写入相应记录。记录分为三种:

记录含义示例
Observation传感器报告的值10:04 时温度为 91.0
Actuation已执行的命令及其结果将阀门设为 80,执行成功
Refusal审批关卡不允许执行的命令Agent X 请求打开阀门 7,遭到拒绝

这些记录中的每个字段都使用已发布词汇表中的术语。SOSA,即 W3C 和 OGC 的传感器本体,定义了 observation 和 actuation 的含义。其他四个词汇表用于补充 SOSA 未涵盖的内容:PROV-O 用于说明请求者,schema.org 用于说明命令如何结束,DCAT 用于说明原始数据的位置,Dublin Core 用于说明审批关卡给出的理由。thingctx 不会自行创造术语。测试会针对发布者提供的文件解析每个术语,如果有术语缺失,测试就会失败。

from thingctx.models.sosa import Observation

obs = Observation(result=91.0, observed_property="temperature")
obs.as_row()  # a flat dict for a table
obs.as_jsonld()  # the same fact, in SOSA terms, for anything that reasons over RDF

只需一个参数即可启用。之后,客户端会将它执行的每次读取、写入、命令、查询和取消,以及审批关卡拒绝的每次操作,都报告为一个 Operation:请求者、操作内容、审批关卡的答复、返回结果和发生时间。记录模型会将这些信息转换为相应记录。SOSA 示例:

from thingctx.models.sosa import recorder

rows: list = []
client = base.guarded(
    pdp, identity={"sub": "urn:agent:planner", "roles": ["operator"]}, record=recorder(rows.append)
)

长时间运行的操作会记录为一个 Actuation:启动时写入尚无结果的记录,结束它的查询或取消操作则会写入带有结果的同一条记录。Operation 位于 thingctx.operations 中,不导入任何内容,并且是运行时与记录模型唯一共享的内容。

记录类是生成的,而非手写的

这三个类来自一个文件: records.yaml,使用 LinkML 编写。该文件列出每个字段、其类型、对应的词汇术语, 以及用于判定两条记录是否相同的字段。运行生成器后,它会写出 Python 类、词汇表和 JSON Schema:

scripts/generate_model.py            # regenerate
scripts/generate_model.py --check    # exit 1 if records.yaml changed and nobody regenerated; CI runs this

目录结构如下。你只需查看三个文件,永远不会接触 _generated:

src/thingctx/models/
    _base.py              hand written. Behaviour every record shares (see below)
    sosa/
        records.yaml      hand written. The model: fields, types, vocabulary terms
        __init__.py       hand written. Exports Observation, Actuation, Refusal
        _generated/       written by the generator. Do not edit
            records.py          the three classes, from gen-pydantic
            records.schema.json JSON Schema, from gen-json-schema
            terms.py            the vocabulary tables, from scripts/generate_model.py

records.py 和 records.schema.json 正是 LinkML 自带命令生成的内容。你可以手动运行 gen-pydantic 或 gen-json-schema,得到完全相同的字节;测试会对此进行检查。terms.py 则由 thingctx 自己的脚本生成, 因为 LinkML 的 RDF 生成器(gen-owl、gen-jsonld-context)会为每个类和字段创建新的术语,而 thingctx 只需要已发布的术语。

_base.py 保存词汇无法描述的行为:记录会拒绝没有时区的时间戳,根据自身内容生成 ID,以便重复写入的相同事实只占一行,并且知道如何将自身转换为表格行或 JSON-LD 文档。它不会提及任何词汇术语。每个生成的类都会继承它。

要为另一种词汇添加模型,请创建 src/thingctx/models/<name>/,在其中放入 records.yaml,运行生成器,并添加一个 recording.py,包含与 SOSA 相同的两个函数:records_for(operation) 和 recorder(write)。生成器、_base.py 和运行时都无需更改。

绑定层永远不会接触词汇

协议绑定通过 HTTP、MQTT 或 rosbridge WebSocket 传输字节。它并不知道有效载荷的含义。上述记录位于绑定层无法导入的层中。pyproject.toml 中的 import-linter 契约会强制执行这一点,因此,导入词汇的绑定会导致构建失败。驱动 Thing 时不会加载记录模型。

扩展

描述一个新系统不需要使用本仓库的任何内容。让 thingctx 学会它尚不了解的功能只需实现一个类,且各部分的工作方式都相同,因此学会一次即可应用于其他部分。

传输是最常见的情况:

class CoapBinding:
    scheme = "coap"

    async def invoke(self, action, form, arguments): ...  # required
    async def read(self, prop, form): ...  # optional

契约由一个 scheme 和一个异步 invoke 组成。如果传输支持,还可以添加 read、write 或 subscribe;运行时会在调用前检查有哪些方法,因此不支持读取的发布订阅传输只需不实现读取方法即可。将绑定传给客户端即可启用。为 thingctx 已提供服务的某个 scheme 注册绑定,会替换内置绑定。

其余部分也采用相同的形式:

要让 thingctx 学会实现使用以下方式验证
新传输ProtocolBindingassert_binding_contract
新的身份验证方式CredentialProviderassert_provider_contract
新的描述来源Registryassert_registry_contract
新的媒体引擎MediaBackendassert_media_backend_contract
用于为一组设备提供服务的新协议GatewayBindingassert_gateway_binding_contract

每个契约都是一个 typing.Protocol:匹配方法即可,无需继承任何内容。@implements(Contract) 会在导入时检查匹配情况,而 thingctx.testing 中的一致性测试套件会检查类型检查器无法验证的运行时行为。examples/13_custom_stack.py 在单个文件中离线实现了每种契约。

你可以将自己编写的内容保留在自己的包中,也可以贡献出来,避免其他人重复编写。仓库外的包通过入口点注册,无需分叉即可被发现,因此只需安装你的包即可:

入口点组提供由以下方式发现
thingctx.bindings一种传输discover_bindings
thingctx.auth出站凭据提供程序discover_auth
thingctx.guards入站网关防护discover_guards
thingctx.gateways用于为一组设备提供服务的总线discover_gateway_bindings
thingctx.local_handlers用于处理 local:// Thing 的进程内处理程序discover_local_handlers

请参阅 docs/BINDINGS.md。

互操作性

thingctx 会使用描述,无论描述由谁生成。 examples/interop/ 下的示例端到端验证了这一点:Eclipse Thingweb 的 node-wot 提供 Thing,thingctx 与之交互;Eclipse Ditto 为数字孪生生成描述,而 thingctx 通过它往返传递状态。Thing Description 是 W3C 推荐标准,因此你在这里编写的文件可移植到任何其他 WoT 使用方,且 thingctx 能读取 W3C Thing Description Directory。

贡献

驱动你自己的系统不需要使用本仓库的任何内容:编写一份描述,然后将 thingctx 指向它。项目真正需要的是它尚不支持的传输,而每种传输只需一个类。

适合入手的地方:

  • good first issue 是背景信息齐全、范围明确的任务。留言认领后,任务就会分配给你。
  • help wanted 列出了其他所有开放任务。
  • CONTRIBUTING.md 说明了绑定的组成方式。

贡献者提交的补丁新增了一条 lint 规则,并弥补了一处测试覆盖缺口。改动小、范围明确,且已合并。

每次提交都需要包含 DCO Signed-off-by 行,可通过 git commit -s 添加。欢迎提交 AI 辅助编写的补丁;每份补丁都会由真人审核并签署。

有问题不一定要提 issue。 Discussions 适合询问“这个已经能做 X 吗”“你们会接受关于 Y 的 PR 吗”,以及如何连接你使用的系统。即使答案是否定的,在那里提问也很有帮助,因为这能让项目了解大家正在尝试做什么。

许可证

Apache 2.0。请参阅 LICENSE。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。