资讯动态

Tinycast 的 MCP 集成:从远程端点与本地 stdio 服务器到 AI 工具调用循环的完整实现指南

发布时间:2026/9/19 15:11:44 来源:尧图企业网站定制
Tinycast 的 MCP 集成从远程端点与本地 stdio 服务器到 AI 工具调用循环的完整实现指南【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycastTinycast 通过 Model Context Protocol 为骨架结合 Tinycast/Features/MCP 下的源码实现系统讲解 Tinycast 的 MCP 架构分层、安全不变式、两种传输通道Streamable HTTP 与 stdio、工具命名规则、工具调用循环、设置界面与手动验证清单读完即可理解并上手配置一个 MCP 服务器并追踪一次完整的工具调用链路。架构分层MCP 与 AI 在何处交汇Tinycast 对 MCP 的定位非常克制一个 MCP 服务器要么是远程 HTTP 端点要么是 Tinycast 在本机 Mac 上拉起的一个命令进程无论哪种形态服务器都会对外公布工具advertise toolsTinycast 按服务器的 handle即slug对这些工具做命名空间隔离再由模型按需调用。这一职责被刻意切成两层Features/MCP/只拥有服务器负责握手、工具列表、调用与生命周期完全不知道聊天是什么AI 功能见 AI 集成文档只拥有工具调用负责把工具描述塞进请求体、把调用结果喂回模型完全不知道 MCP 的存在。两层唯一的交汇点是AIChatCoordinator.send。从源码结构看MCP 被设计成一个可以被整体替换或整体关闭的旁路模块而不是散落在聊天逻辑里的分支。安全不变式Tinycast 的 MCP 设计底线原文档用一整节Invariants陈述了 MCP 功能的十条设计底线它们共同回答了一个可执行代码的通道在桌面应用里如何被安全地限制这一问题。以下是每条不变式及其源码佐证。1. 默认全关关闭就是彻底关闭AppSettings.mcpEnabled是唯一总开关而MCPCoordinator.applyEnabled()是把它投射到运行时的唯一位置见 MCPCoordinator.swift关闭时不会建立任何连接、不会驻留任何本地进程、不会向任何模型暴露任何工具。aiEnabled关闭也有同样效果因为聊天是 MCP 的唯一消费者——isActive的定义就是settings.aiEnabled settings.mcpEnabled。关键的安全细节mcpEnabled、aiEnabled与mcpServers都被排除在设置备份之外见 SettingsBackupCoverage.swift。理由很直接——一份服务器清单既是可执行代码的来源又是聊天上下文的去向开关本身同时充当允许运行的同意凭证因此任何导入的备份都不可能带着一个已经连通的服务器抵达。2. 凭据只存在于登录钥匙串Login KeychainMCPServer在UserDefaults中持久化的只有端点、HTTP 头名称、命令、命令参数和环境变量名称——它从不包含任何秘密本身见 MCPServer.swift。HTTP 头值与每个环境变量的值被编码成一个 JSON 条目、按服务器存放在KeychainSecretStore.mcpSecrets下见 MCPSecretStore.swift并且永不进入日志、错误信息或备份。读取失败时按无秘密处理让服务器在连接阶段失败而不是在存储层报错。3. 远程端点强制 HTTPS远程端点复用 AI 提供方同款的AIEndpointPolicy.validate只有localhost、127.0.0.1与::1允许明文 HTTP不允许其他任何 scheme见 MCPHTTPTransport.swift。整个项目只有这一个判定点MCP 不另设第二套杜绝了策略分叉。4. 工具调用永不进入对话历史完整的调用-结果往返只存活在一个回合turn内发生在AIToolLoopProvider中ChatSession保存的只是一个ChatToolUse渲染记录与ChatSearch完全同构。这是刻意的与结果分离的tool_call、或调用落在boundedContext之外的调用结果都是两个提供方都会拒绝的请求形态所以那种能产生这种形态的数据结构永远不会被写下来。额外收益是后续回合看到的是模型自己的回答而不是它已经为原始工具输出付过一次费的原文。5. 对话框可以授予服务器只有设置页可以永久拒绝MCPTrust默认是.ask每次会话的第一次调用会经过 Tinycast 自有的三选一对话框见 MCPCoordinator.swiftAlways Allow持久化为.alwaysAllow This Chat仅对当前ChatSession.id授予下一次会话会再次询问Dont AllowEscape 键就是它只拒绝这一次调用下一次调用仍会再问。Escape 永远不会被允许持久化任何决定而.never只能在服务器的设置行上被设置。整套规则浓缩在纯函数MCPTrustPolicy.decide中见 MCPTrustPolicy.swift.never直接拒绝、.always直接放行、.ask取决于本次聊天是否已授予。6. 被拒绝或失败的调用是内容不是抛出错误一次被拒或失败的调用以AIToolResult形式返回给模型模型可以读取并绕行见 MCPCoordinator.swift。因此用户拒绝了这个工具调用最终变成一句诚实的话进入对话而不是一次失败的回合并打断流程。7. 每个回合有三重边界AIToolLoopProvider.maxRounds为 10超过后回合失败并明确说明——一个只调工具不回答的模型会被截停。每条结果被截断到maxResultBytes一个回合所有结果合计被截断到maxTurnResultBytes因为工具输出是回合内追加的永远不会经过ChatSession.boundedContext的常规约束。8. handle 是派生出来的不是输入的MCPSlug从服务器名称派生 handle 并保证唯一见 MCPServer.swift小写化、字母数字保留、其他字符折叠为单个-、最长 24 字符、空名回退为server重名时追加-2到-99的后缀极端情况用 8 位 UUID 兜底。MCPSettingsStore.normalized在每次保存时重新派生 slug保证slug永远不可能同时指代两个服务器或什么都不指代。未知 handle 不是地址nosuch hello会被原样发送不触发任何命名空间逻辑。9. 服务器随聊天启动闲置十分钟后停止stdio 服务器是一个由他人代码构成的常驻进程100 MB 内存预算正是随启动而启、而非随启动而驻留的原因。MCPServerManager维护 600 秒闲置计时器见 MCPServerManager.swift每次使用都会重置它进程在prepareForTermination()时也会立即清理。10. 只有两种 HTTP 形态会获得工具AIModelCapabilities.tools对.api为 true、对.appleIntelligence和.chatGPT为 falseCodex 路线本就有工具不可用的不变式而且 MCP 无权解除本地 CLI 上的沙箱边界。结果就是在 Apple Intelligence 或 ChatGPT 模型上没有任何工具会被提供回复照常流式输出。11. Tinycast 不向服务器暴露任何能力服务器发来的采样sampling、elicitation、roots 等反向请求一律以 JSON-RPC 错误拒绝见 MCPProtocol.swift错误码为-32601消息为 Tinycast exposes no MCP capabilities.initialize握手里的capabilities为空字典见 MCPServerConnection.swift。12.Model/保持纯 Foundationmcp-test编译发布的模型代码并钉死 framing、handle、工具名、输出展平、信任与寻址行为mcp-stdio-test驱动真实的子进程。这条不变式保证了协议层可被独立验证。传输通道同一套 JSON-RPC两种 framing两种传输都用同一个编码器MCPProtocol讲 JSON-RPC 2.0只有 framing 不同。协议版本固定为2025-06-18见 MCPProtocol.swift。维度MCPHTTPTransportMCPStdioTransport形态每条消息一次 POST通过进程 stdin/stdout 的换行分隔消息回复JSON body或用 AI 层的SSEParser读取 SSE 流按 id 匹配的一行会话从任意响应捕获Mcp-Session-Id并在后续请求回放进程本身就是会话超时15 秒tools/call为 60 秒相同按每个挂起请求计拆除丢弃会话关闭 stdin一秒后补发 SIGTERMHTTP 传输的实现细节MCPHTTPTransport在 MCPHTTPTransport.swift 中实现tools/call之外的方法超时为 15 秒、tools/call为 60 秒每个请求带上MCP-Protocol-Version会话 id 每次响应都读取因为会话可能在任何一次响应中开启用临时URLSession无缓存、绕过本地与远程缓存发送请求。会话失效由服务器决定收到 404 且已有会话 id 时清空会话 id 并在下一次请求重开新会话。认证失败401/403会被翻译成服务器拒绝了 Tinycast 的凭据。SSE 响应体被逐帧解析为独立消息JSON body 则整体解析为一条消息。stdio 传输的实现细节MCPStdioTransport见 MCPStdioTransport.swift复用了CodexAppServerClient的机制只是换了一个协议一个 pending-id 映射表配每请求看门狗、一条未终止行超过 8 MB 即触发防护、cleanup会失败所有等待中的 continuation。有两个细节是承重的终止可能抢先于最后一次 stderr 读取所以didExit会先排空管道再组装错误消息——服务器退出前打印的内容是读者唯一能看到的原因stderr 缓冲区上限为 8 KB。挂起调用会先于所有者被告知而失败因为所有者自己的close()会把真实原因覆盖成 not running。命令的查找由Platform/ExecutableLocator完成遍历 PATH、常见安装前缀和每一个 nvm Node 版本最后才询问登录 shell——因为 GUI 应用继承的是 Finder 的 PATH上面根本没有npx、uvx或node。同时启动环境会把可执行文件目录、/opt/homebrew/bin、/usr/local/bin并入 PATH并强制设置NO_COLOR1避免服务器输出 ANSI 转义污染协议流见 MCPStdioTransport.swift。连接状态机MCPServerConnection见 MCPServerConnection.swift把每次握手串成状态机stopped→connecting→ready(tools:)或failed(message)。启动时依次发送initialize带protocolVersion、空capabilities与clientInfo、notifications/initialized通知然后tools/list拉取工具并更新ready(tools: count)。服务器在运行中增减工具只会通过notifications/tools/list_changed通知告知收到后连接会重新拉取一次工具列表。isIdle同时覆盖stopped与failed因此下次进入聊天时会自动重试曾经断网失败的服务器。工具命名slug__tool的 64 字符上限MCPToolName是把服务器 handle 与工具自身名称合并成提供方可接受单一标识符的唯一位置slug__tool净化到[A-Za-z0-9_-]并截断到 64 字符OpenAI 的上限也是两者中更紧的那个见 MCPTool.swift。需要截断时存活下来的是 handle 那一半因为 handle 负责路由回正确的服务器。parse通过分隔符反向切出 slug 与工具名没有分隔符的名称被认为从来不是我们的。MCPTool.aiTool还携带显示对——服务器的标题与工具自身名称——这样 AI 层渲染一行时不需要解析任何线上的名字。工具列表解析时会丢弃畸形条目缺失的inputSchema会回退为{type: object}。输出展平在MCPToolOutput.flatten见 MCPTool.swift中完成text与resource块保留文本resource_link以[resource uri]形式命名image/audio块只留一句[image content omitted]——因为只有文本模型能消费的内容才会内联服务器只回结构化内容时整体按 JSON 文本交付isError标志被保留以便模型感知失败。工具调用循环AIToolLoopProviderAIToolLoopProvider是一个包装另一个AIProvider的AIProvider这正是AIChatState几乎无需改动、未包装的路由行为完全不变的原因。每一轮它流式传输底层路由透传文本、思考与用量同时收集.toolCallRequested如果没有调用请求产出.finished并结束。否则它追加带调用的助手回合然后对每个调用依次产出.toolCall、等待调用方即MCPCoordinator.invoke、产出.toolResult并追加工具回合——然后带着同一组工具继续下一轮直到maxRounds上限。两个工具事件是刻意分离的.toolCallRequested是传输层发出的只携带线上名称循环消费它但绝不转发.toolCall是循环取而代之发出的已经携带了转录行需要展示的一切。AIRequestBody负责构建各提供方的 JSON它是纯函数并被 harness 钉死因为形态不容含糊OpenAI 接受{type: function, function: {…}}目录以及独立的role: tool回合作为结果Anthropic 接受无包装的input_schema、input是解析回对象的参数的tool_use内容块以及无论多少条都必须合并为一个用户回合的tool_result块。设置界面Settings → AI 中的 MCP 区块MCPSettingsSection是 Settings → AI 里的一个区块位置与AICommandSection类似。每一行以 handle 领衔——因为那是用户要亲手输入的一半——随后是实时状态与传输方式。状态由MCPServerStatus表示见 MCPTransport.swiftStopped、Connecting…、N tools或失败消息。MCPServerEditor是编辑面板包含名称、HTTP 或命令形态、凭据、启用开关、信任级别以及一个Test Connection按钮它会跑一次真实握手让拼写错误在会话中途之前就被发现。服务器配置的核心字段来自MCPServer与MCPTransportKindHTTP 形态url端点、headerName认证头名称默认Authorization、头值存钥匙串stdio 形态command可执行命令如npx、arguments参数数组、environmentKeys环境变量名列表值存钥匙串。信任级别MCPTrust的三个取值在 MCPServer.swift 中定义Ask Each Chat默认、Always Allow、Never Allow。手动验证清单原文档以一份手动清扫清单收尾它同时也是对本文所有机制的端到端验收HTTP 服务器带 bearer 头从 Test Connection 与自身行内都能报告工具数量stdio 服务器npx -y modelcontextprotocol/server-filesystem ~/Desktop能到达 ready调色板关闭十分钟后进程消失Quit 时立即消失一次用工具回答的问题行内显示该工具行先是 spinner 后是 glyph回复在其后继续从 ⌘K → Chat History 重新打开该聊天仍能看到当时跑了什么第一次调用弹出对话框Allow This Chat 在本次会话不再询问、下次会话再问Always Allow 跨重启存活Escape 只拒绝那一次调用filesystem list my desktop发送时去掉前缀、显示 chip且只提供该服务器自己的工具nosuch hello原样发送Apple Intelligence 或 ChatGPT 模型上不提供任何工具回复照常流式输出先关 MCP、再关 AI不留下任何服务器进程驻留一份设置备份既不携带服务器也不携带开关。自动化验证由三组 harness 承担mcp-test与mcp-stdio-test分别钉死模型层与真实子进程ai-provider-test覆盖目录与回合编码、分片参数解码ai-chat-test覆盖循环、轮次上限、输出边界与工具使用持久化。快速上手添加并验证一个 MCP 服务器结合上述机制一条完整的上手路径是在 Settings → AI 中找到 MCP 区块新增服务器输入名称如filesystemslug 会由MCPSlug自动派生为filesystem选择 stdio 形态填入命令npx、参数-y modelcontextprotocol/server-filesystem与~/Desktop点击 Test Connection确认行内状态变为工具数量就绪首次握手 tools/list完成在任意聊天中以filesystem引用该服务器首个工具调用会触发信任对话框选择 Allow This Chat 或 Always Allow观察模型以filesystem__tool形式的线上名称调用工具行内出现调用结果回复在结果之后继续关闭调色板等待十分钟确认进程已被闲置计时器回收。整套设计的核心可概括为一句话Tinycast 把运行他人代码这件事压缩进一个默认关闭、凭据隔离、HTTPS 强制、信任可审计、生命周期与聊天绑定的窄通道里而Features/MCP/与 AI 层之间唯一的接口就是 AIChatCoordinator.swift 的send。【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价