How to Track Agent Skill and MCP Server Changes (and Pin Versions)
October 31, 2026 · backskills · 7 min read
To stop a third-party skill or MCP server from changing under you, pin it: a SKILL.md to a git commit SHA, an MCP server to an exact package version. Then watch upstream for new versions so you upgrade on purpose, after reviewing the diff, instead of finding out from a broken agent.
Two jobs. Pin. Watch. The rest of this post is how.
Why this matters more than it does for normal dependencies
A library change usually breaks loudly. A type error, a failing test.
A skill change usually doesn't. Someone rewrites a description and your agent stops loading the skill for half its old triggers. Someone adds a step and outputs change shape. An MCP server renames a tool or edits its description, and the model picks a different tool. Nothing throws. Behavior just drifts.
And the model under it drifts too. Chen, Zaharia and Zou (2023) compared versions of the same hosted LLM service a few months apart and found behavior on the same tasks changed substantially. If both the skill and the model can move, you want at least one of them nailed down.
Skill pinning means referencing an exact, immutable revision of a skill (a commit SHA or a published version) rather than "whatever is on main."
Step 1: Pin SKILL.md files to a commit
Branch names move. Tags can be moved. Commit SHAs can't.
Vendor the skill at a known SHA:
#!/usr/bin/env bash
# vendor-skill.sh OWNER/REPO PATH/TO/SKILL SHA
set -euo pipefail
repo="$1"; path="$2"; sha="$3"
name="$(basename "$path")"
dest="skills/$name"
tmp="$(mktemp -d)"
git clone --filter=blob:none --no-checkout "https://github.com/$repo" "$tmp"
git -C "$tmp" checkout "$sha" -- "$path"
rm -rf "$dest" && mkdir -p skills && cp -r "$tmp/$path" "$dest"
echo "$repo $path $sha" >> skills/LOCK
echo "pinned $name @ $sha"
Keep a lockfile so anyone can see what's installed and from where:
# skills/LOCK (repo path sha)
anthropics/skills skills/pdf 3f2a9c1e...
someorg/agent-skills release-notes 9b81d07a...
Now upgrades are a pull request. You see the diff of SKILL.md and every script before your agent does.
Step 2: Pin MCP servers to an exact version
The MCP Registry versioning docs say each publication must have a unique version string, and published metadata can't be changed afterward. That's what makes pinning possible. Use it.
The common mistake is a client config that installs "latest" on every start:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "@my-username/mcp-weather-server"]
}
}
}
Pin it:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "@my-username/mcp-weather-server@1.0.1"]
}
}
}
Same idea for Python (uvx package==1.0.1) and containers (pin by digest, image@sha256:..., not by tag).
Remote MCP servers are harder. You connect to a URL, and the operator can deploy whenever they like. You can't pin those. You can only watch them, which is step 3.
Step 3: Watch upstream
You have pins. Now you need to know when a newer version exists, or when something you can't pin has changed.
DIY: poll GitHub for skill changes
#!/usr/bin/env python3
"""Report pinned skills whose upstream path has new commits."""
import os, httpx
TOKEN = os.environ.get("GITHUB_TOKEN")
H = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
def latest_sha(repo: str, path: str) -> str:
r = httpx.get(f"https://api.github.com/repos/{repo}/commits",
params={"path": path, "per_page": 1}, headers=H)
r.raise_for_status()
return r.json()[0]["sha"]
for line in open("skills/LOCK"):
if line.startswith("#") or not line.strip():
continue
repo, path, pinned = line.split()
head = latest_sha(repo, path)
if not head.startswith(pinned[:7]):
print(f"CHANGED {repo}/{path}: pinned {pinned[:7]} -> upstream {head[:7]}")
DIY: poll the MCP Registry
The registry has a public search API (quickstart):
curl -s "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.my-username/weather"
Compare the returned version with your pin and alert when it differs. Run both checks daily in CI (a scheduled GitHub Action works) and open an issue when something moved.
This works. It also becomes a chore around the tenth dependency: rate limits, repos that move, skills nested at odd paths, remote servers with no version field you can rely on.
Managed: backskills
backskills does the watching for public skills and servers. It tracks SKILL.md files on GitHub and servers in the MCP Registry, checks them daily, keeps version history so you can pin to a known revision, and sends a signed webhook within 24 hours when something you watch changes. Agents can use it directly through the MCP endpoint at https://backskills.ai/mcp; the llms.txt describes what's available.
Step 4: Verify the webhook signature
Any change-alert webhook should be signed, and you should reject anything that isn't. backskills signs every delivery with your per-key secret (get it from GET /api/webhooks/secret with your X-Agent-Key). Each request carries three headers:
X-Backskills-Event: for exampleskill.changedX-Backskills-Timestamp: Unix seconds when the attempt was sentX-Backskills-Signature:sha256=followed by the hex HMAC-SHA256 of{timestamp}.plus the raw request body, keyed with yourwhsec_...secret
Including the timestamp in the signed string lets you reject replays. Send yourself a real signed alert with POST /api/webhooks/test and verify it like this:
import hmac, hashlib, os, time
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = os.environ["BACKSKILLS_WEBHOOK_SECRET"].encode() # the whsec_... value
@app.post("/hooks/skill-changed")
async def skill_changed(req: Request):
body = await req.body() # raw bytes, before JSON parsing
ts = req.headers.get("X-Backskills-Timestamp", "")
sent = req.headers.get("X-Backskills-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300: # 5-minute replay window
raise HTTPException(401, "stale timestamp")
expected = "sha256=" + hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sent, expected):
raise HTTPException(401, "bad signature")
event = await req.json()
# open an issue, ping Slack, or kick off your regression tests
return {"ok": True}
Two rules: sign the raw bytes, not re-serialized JSON, and never use == for the comparison.
Step 5: Review before you bump
When an alert fires, don't just bump the pin. Five minutes of review:
- Read the SKILL.md diff. Did the
descriptionchange? That changes when the skill triggers. - Read script diffs. New network calls, new dependencies, new file writes. Treat it like any third-party code.
- For MCP servers, diff the tool list. Names, descriptions, input schemas. A changed description can change which tool the model picks.
- Run your trigger tests (the should-trigger / should-not-trigger lists from How to write a SKILL.md) on every model you use.
- Bump the pin in a PR with a one-line note on what changed.
A tiny tool-list differ for MCP servers, using the official Python SDK client:
import asyncio, json, sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def tools(cmd: list[str]) -> dict:
params = StdioServerParameters(command=cmd[0], args=cmd[1:])
async with stdio_client(params) as (r, w):
async with ClientSession(r, w) as s:
await s.initialize()
res = await s.list_tools()
return {t.name: {"desc": t.description, "schema": t.inputSchema}
for t in res.tools}
async def main():
old = await tools(["npx", "-y", sys.argv[1]])
new = await tools(["npx", "-y", sys.argv[2]])
for name in sorted(set(old) | set(new)):
if old.get(name) != new.get(name):
print(f"~ {name}")
print(" old:", json.dumps(old.get(name))[:200])
print(" new:", json.dumps(new.get(name))[:200])
asyncio.run(main())
# python diff_tools.py @org/server@1.0.1 @org/server@1.1.0
For authors: make this easy for your users
If you publish skills or servers, you can make everyone downstream safer:
- Bump
metadata.version(skills) orversion(server.json) on every behavior change. - Keep a CHANGELOG. Flag description changes explicitly, since they change triggering.
- Tag releases so people can pin to something readable.
- Add a status badge to your README (backskills has author badges) so users can see the current version at a glance.
FAQ
How do I know when an agent skill I use has changed? Pin it to a commit, then either poll the GitHub commits API for its path daily or use a tracker like backskills that sends a signed webhook within 24 hours of a change.
How do I pin an MCP server version? Put the exact version in the install command (package@1.0.1, package==1.0.1, or an image digest). Avoid bare package names that resolve to latest.
Can an MCP server change its tools without me knowing? Yes, if you run unpinned or use a remote server. Pin where you can, and diff the tool list when a new version appears.
How do I roll back a skill? If you vendored it at a SHA, revert the PR that bumped the pin. That's the whole point of the lockfile.
Is version history available for skills that don't publish versions? Git history is the version history. Trackers like backskills record each observed revision, so you can pin even when the author never tagged anything.
Start watching the skills and MCP servers your agents depend on, free: backskills.ai.