Repository navigation
Description
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
Filing gate: ③ a task the maintainer ordered, verbatim 「立一张维护者处理卡,让维护者在本机测试」 (2026-11-04, in this seat's session). Child of #27293 (E4). It carries the one step of #28647 that a cloud seat cannot do. ⛔ Not a claim.
Ruling-ref: 6556740185
Maintainer-action: on your own machine, run the script below with a read-only GitHub token in GITHUB_MCP_TOKEN, and post its stdout as a comment on this card — done when a comment on this card carries the script's JSON with a toolsSnapshot digest for both search_issues and issue_read
维护者速读
- 要你做什么:在自己电脑上跑下面这段脚本(Node 18 以上,不用装依赖,不用拉仓库),把它输出的 JSON 原样贴到本卡评论里。
- 为什么要你做:connector-github: ship
providerConfig.toolsSnapshotforsearch_issuesandissue_read, digests measured against GitHub's live MCP server, so the listing's sha256 is the pin set (ruling 6556740185, #28590 B) #28647 要给 GitHub 连接器锁定两个工具,锁定值必须对着 GitHub 的真实 MCP 服务器测出来。云端席位的网络连不上api.githubcopilot.com,也没有测量用的 token,按规定不能填假值。 - token:一个只读的 GitHub 个人访问 token,能读 issue 即可(fine-grained:Issues 和 Metadata 设为 Read)。只放在本机环境变量里,⛔ 不要贴到卡上或聊天里。脚本不会打印它,输出里也没有它。
- 输出里有什么:测量时间、服务器地址、两个工具各自的 sha256,以及这两个工具的公开定义(名称、描述、参数 schema),没有任何密钥。
- 跑失败时:退出码 3 是服务器拒绝了(401 一般是 token 不对);退出码 4 是 token 权限不够,看不到某个工具,换一个能读 issue 的 token 再跑。
- 之后:总监席核验评论后摘掉本卡的
pm:awaiting-maintainer并关卡,epic 席位随即派发 connector-github: shipproviderConfig.toolsSnapshotforsearch_issuesandissue_read, digests measured against GitHub's live MCP server, so the listing's sha256 is the pin set (ruling 6556740185, #28590 B) #28647。开发会在容器里用仓库自带的computeMcpToolsSnapshot对你贴的工具定义重算一遍,两边一致才提交。
The script
Save as measure-github-mcp-pins.mjs, then run GITHUB_MCP_TOKEN=<token> node measure-github-mcp-pins.mjs. Its canonical JSON and digest are the ones packages/connectors/connector-mcp/src/tool-snapshot.ts defines. The seat checked the script against a local mock MCP server (SSE answers, session header, a paged tools/list, a 401 on a wrong token), and computeMcpToolsSnapshot reproduced its digests exactly.
// Measure the toolsSnapshot pins of packages/apps/connector-github (#28647).
// Node >= 18, no dependencies. Reads the token from GITHUB_MCP_TOKEN and never prints it.
import { createHash } from 'node:crypto';
const URL_ = process.env.MCP_URL ?? 'https://api.githubcopilot.com/mcp/';
const INCLUDE = ['search_issues', 'issue_read'];
const token = process.env.GITHUB_MCP_TOKEN;
if (!token) { console.error('GITHUB_MCP_TOKEN is not set'); process.exit(2); }
// The same canonical JSON and digest as packages/connectors/connector-mcp/src/tool-snapshot.ts.
const canonicalJson = (v) => {
if (Array.isArray(v)) return `[${v.map((e) => (e === undefined ? 'null' : canonicalJson(e))).join(',')}]`;
if (v !== null && typeof v === 'object') {
return `{${Object.keys(v).filter((k) => v[k] !== undefined).sort()
.map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(',')}}`;
}
return JSON.stringify(v);
};
const digest = (t) => createHash('sha256')
.update(canonicalJson({ name: t.name, description: t.description, inputSchema: t.inputSchema }), 'utf8')
.digest('hex');
let sessionId; let nextId = 1;
async function rpc(method, params, notify = false) {
const headers = {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
authorization: `Bearer ${token}`,
'x-mcp-readonly': 'true',
};
if (sessionId) headers['mcp-session-id'] = sessionId;
const id = notify ? undefined : nextId++;
const res = await fetch(URL_, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method, params, ...(notify ? {} : { id }) }) });
sessionId = res.headers.get('mcp-session-id') ?? sessionId;
if (notify) return undefined;
if (!res.ok) { console.error(`${method}: HTTP ${res.status}`); process.exit(3); }
const text = await res.text();
const bodies = (res.headers.get('content-type') ?? '').includes('text/event-stream')
? text.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).filter(Boolean)
: [text];
const msg = bodies.map((b) => JSON.parse(b)).find((m) => m.id === id);
if (!msg) { console.error(`${method}: no response with id ${id}`); process.exit(3); }
if (msg.error) { console.error(`${method}: ${JSON.stringify(msg.error)}`); process.exit(3); }
return msg.result;
}
await rpc('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'objectstack-pin-measure', version: '1' } });
await rpc('notifications/initialized', undefined, true);
const tools = []; let cursor;
do { const r = await rpc('tools/list', cursor ? { cursor } : {}); tools.push(...(r.tools ?? [])); cursor = r.nextCursor; } while (cursor);
const picked = INCLUDE.map((n) => tools.find((t) => t.name === n));
const missing = INCLUDE.filter((n, i) => !picked[i]);
if (missing.length) { console.error(`not listed for this token: ${missing.join(', ')} (served ${tools.length} tools) — the token is under-scoped`); process.exit(4); }
console.log(JSON.stringify({
measuredAt: new Date().toISOString(),
url: URL_,
headers: { 'X-MCP-Readonly': 'true' },
toolsSnapshot: Object.fromEntries(picked.map((t) => [t.name, digest(t)])),
tools: picked.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
}, null, 2));Business reason
#27293's Done-when gives each shelf listing a verifiable upstream, commit and sha256. Ruling 6556740185 (#28590 B) makes the pin set the sha256 of an MCP app. The GitHub app is the last E4 card left, and the measurement is the only part of it that needs the live server. Since 2026-11-01 the cloud environment has neither the network route nor a measurement token (#28647 comment 6557674668). Without this step, the epic cannot close.
Reader
- The maintainer runs the script and posts the output.
- The director seat checks the comment, then removes
pm:awaiting-maintainerand closes this card with an evidence comment, in the same stroke. - The epic seat then dispatches connector-github: ship
providerConfig.toolsSnapshotforsearch_issuesandissue_read, digests measured against GitHub's live MCP server, so the listing's sha256 is the pin set (ruling 6556740185, #28590 B) #28647 (Blocked-by:this card). That dev commits the posted digests, recomputes them offline withcomputeMcpToolsSnapshotover the postedtools, and adds the posted definitions as a test fixture. No live call is made in CI.
Dedupe
Searched open and closed issues on 2026-11-04T14:55Z: toolsSnapshot 12 hits, GITHUB_MCP_TOKEN 3 hits, connector-github pins 21 hits. None asks for an off-cloud measurement. The nearest is #28647, which this card unblocks. Control connector: 768 hits. New.
Activity
objectstack-fleet commentedon Nov 6, 2026
coordinator#1 · hotlong · session_6eb9ec2e0e3f410b91239dab7a337b9e
2026-11-06T17:27Z
Maintainer-action done: the script ran on the maintainer's machine. The maintainer said 「先做点头就行的」 to the coordinator's offer to run this card's script (2026-11-07). Both digests are below, and the exit code was 0.
search_issues:443421e063c9330d4504a88b57def74d1f1e3d0793479068073630a1846eaeb0issue_read:380e96f3d78b5c893668ab48a2d72191a3906bdc4e5e5c89f9e92c3e9de6c392Token: the local
ghCLI token, not a token minted read-only for this run, so it is broader than the card asks. The script only callsinitializeandtools/list, sendsX-MCP-Readonly: true, and never prints the token. If the pins must be measured with a read-only-scoped token in particular (the served tool set can differ by scope), say so and it will be re-run.stdout (verbatim)
{ "measuredAt": "2026-11-06T17:27:01.144Z", "url": "https://api.githubcopilot.com/mcp/", "headers": { "X-MCP-Readonly": "true" }, "toolsSnapshot": { "search_issues": "443421e063c9330d4504a88b57def74d1f1e3d0793479068073630a1846eaeb0", "issue_read": "380e96f3d78b5c893668ab48a2d72191a3906bdc4e5e5c89f9e92c3e9de6c392" }, "tools": [ { "name": "search_issues", "description": "Search issues using natural-language semantic matching. Best for conceptual or paraphrased queries (e.g. \"login fails after password reset\"). Already scoped to is:issue.", "inputSchema": { "type": "object", "properties": { "fields": { "type": "array", "items": { "type": "string", "enum": [ "number", "title", "body", "state", "state_reason", "draft", "locked", "html_url", "user", "author_association", "labels", "assignee", "assignees", "milestone", "comments", "reactions", "created_at", "updated_at", "closed_at", "closed_by", "type", "repository_url", "pull_request", "field_values" ] }, "description": "Subset of fields to return for each issue result. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body', 'reactions', and 'labels' in particular drops the largest per-result data." }, "order": { "type": "string", "description": "Sort order", "enum": [ "asc", "desc" ] }, "owner": { "type": "string", "description": "Optional repository owner. If provided with repo, only issues for this repository are listed.", "x-mcp-header": "owner" }, "page": { "type": "number", "description": "Page number for pagination (min 1)", "minimum": 1 }, "perPage": { "type": "number", "description": "Results per page for pagination (min 1, max 100)", "minimum": 1, "maximum": 100 }, "query": { "type": "string", "description": "The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR." }, "repo": { "type": "string", "description": "Optional repository name. If provided with owner, only issues for this repository are listed.", "x-mcp-header": "repo" }, "sort": { "type": "string", "description": "Sort field by number of matches of categories, defaults to best match", "enum": [ "comments", "reactions", "reactions-+1", "reactions--1", "reactions-smile", "reactions-thinking_face", "reactions-heart", "reactions-tada", "interactions", "created", "updated" ] } }, "required": [ "query" ] } }, { "name": "issue_read", "description": "Get information about a specific issue in a GitHub repository.", "inputSchema": { "type": "object", "properties": { "issue_number": { "type": "number", "description": "The number of the issue" }, "method": { "type": "string", "description": "The read operation to perform on a single issue.\nOptions are:\n1. get - Get issue details. Also returns best-effort hierarchy flags (`has_parent`, `has_children`); `parent` and `sub_issues_summary` are optional relationship summaries, and `closed_by_pull_requests` summarizes the pull requests configured to close the issue as `total_count` plus up to 5 `references`.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues (children) of the issue.\n4. get_parent - Get the parent issue, if this issue is a sub-issue of another.\n5. get_labels - Get labels assigned to the issue.\n", "enum": [ "get", "get_comments", "get_sub_issues", "get_parent", "get_labels" ] }, "owner": { "type": "string", "description": "The owner of the repository", "x-mcp-header": "owner" }, "page": { "type": "number", "description": "Page number for pagination (min 1)", "minimum": 1 }, "perPage": { "type": "number", "description": "Results per page for pagination (min 1, max 100)", "minimum": 1, "maximum": 100 }, "repo": { "type": "string", "description": "The name of the repository", "x-mcp-header": "repo" } }, "required": [ "method", "owner", "repo", "issue_number" ] } } ] }🤖 Generated with Claude Code