Skip to content

maintainer-action: measure the two MCP tool pins of connector-github on a local machine and post the output (unblocks #28647) #30116

Open

Description

@objectstack-fleet

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

维护者速读

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

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

  1. added
    pm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queue
    pm:awaiting-maintainerAwaiting a manual maintainer action — ruling already given; no dispatch, no machine exit
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 4, 2026
  2. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Nov 4, 2026
  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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: 443421e063c9330d4504a88b57def74d1f1e3d0793479068073630a1846eaeb0
    • issue_read: 380e96f3d78b5c893668ab48a2d72191a3906bdc4e5e5c89f9e92c3e9de6c392

    Token: the local gh CLI token, not a token minted read-only for this run, so it is broader than the card asks. The script only calls initialize and tools/list, sends X-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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:automationdomain:serverDomain lane — seat card indexed by label:pm:seatpm:awaiting-maintainerAwaiting a manual maintainer action — ruling already given; no dispatch, no machine exitpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queuepriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions