Extensions — Packaged Superpowers for Gemini CLI
An extension bundles an MCP server, slash commands, persistent context, and agent skills into a single installable unit. One gemini extensions install command and your CLI gains an entirely new capability domain — from Obsidian vault RAG to network packet analysis. This module shows you how extensions work, how to build one from scratch, and the community extensions worth installing today.
What You'll Learn
- Understand what an extension packages and how it integrates with the Gemini CLI runtime
- Install, list, and remove extensions with the
gemini extensionssubcommand - Read the anatomy of an extension: file structure and
package.jsonconfig schema - Build a complete "GitHub PR Assistant" extension with skill files and a live MCP server
- Publish your extension to the geminicli.com gallery
- Know which community extensions are worth installing for real-world workflows
What Are Extensions?
Before VS Code extensions, you had a capable editor — but to add Git lens, a Docker sidebar, or a spell checker, you had to manually configure each tool, write custom keybindings, and wire up language servers yourself. The pain was that every developer had a different local setup, and sharing a "good configuration" meant emailing a dotfile. VS Code extensions solved this by packaging everything together: the UI, the commands, the language server, the settings defaults.
Gemini CLI extensions follow the exact same model. The mapping: an extension is a self-contained directory published to GitHub that bundles a MCP serverModel Context Protocol server — a process that exposes tools (functions Gemini can call) and resources (data Gemini can read) over a standardized JSON-RPC protocol. MCP is what lets Gemini "reach outside" the conversation to interact with real systems., slash command skill files, a gemini.md context document, and optional theme overrides. One install command and your CLI gains a fully wired capability domain.
A Gemini CLI extensionA versioned npm-style package (published to GitHub or geminicli.com) that Gemini CLI can install. At install time, the CLI clones the repo, reads package.json for the gemini.extension config block, registers MCP servers, copies skill files into the session skill registry, and merges gemini.md into the context stack. is a versioned directory with a package.json containing a gemini.extension config block. When installed, the CLI:
- Clones the GitHub repo to
~/.gemini/extensions/<name>/ - Registers any declared MCP servers so they auto-start with each session
- Adds skill
.mdfiles to the slash command registry (e.g.,/pr-review) - Merges the extension's
gemini.mdat the bottom of the context stackThe ordered set of gemini.md files that Gemini CLI loads at session start: global (~/.gemini/GEMINI.md) → project (.gemini/GEMINI.md) → extension gemini.md files. All are concatenated into the system prompt.
Without extensions, adding a new MCP server to your workflow means editing ~/.gemini/settings.json manually, figuring out start commands, and writing skill files by hand. A single developer spending 30 minutes on this configuration can package it as an extension so every teammate gets it in under 10 seconds. The Obsidian extension, for example, eliminates the manual setup of a local RAG pipeline — something that previously took 2+ hours of configuration.
Installing, Listing, and Removing Extensions
The gemini extensions subcommand handles the full extension lifecycle. Extensions are installed globally (available in all projects) but can also be pinned per-project via .gemini/settings.json.
Core Commands
# Install from GitHub URL
gemini extensions install https://github.com/example/gemini-ext-obsidian
# List all installed extensions
gemini extensions list
# Remove an extension by name
gemini extensions remove gemini-ext-obsidian
# Update all extensions to latest
gemini extensions update
# Show details about a specific extension
gemini extensions info gemini-ext-obsidian
# Install from GitHub URL
gemini extensions install https://github.com/example/gemini-ext-obsidian
# List all installed extensions
gemini extensions list
# Remove an extension by name
gemini extensions remove gemini-ext-obsidian
# Update all extensions to latest
gemini extensions update
# Show details about a specific extension
gemini extensions info gemini-ext-obsidian
install clones the repo to ~/.gemini/extensions/ and registers everything declared in package.json. Subsequent sessions automatically start the extension's MCP servers.
gemini extensions install with the same caution as npm install -g.
Walkthrough: Installing the Obsidian Extension
The Obsidian extension turns your Obsidian vaultA local directory of Markdown files managed by the Obsidian note-taking application. Vaults can contain thousands of interconnected notes with backlinks, tags, and metadata. into a queryable RAG knowledge base without any configuration.
Install the extension
gemini extensions install https://github.com/example/gemini-ext-obsidian
Point it at your vault — the extension adds a config prompt on first run:
gemini
# Extension: Obsidian RAG
# Vault path not configured. Enter path: ~/Documents/MyVault
# Indexing 2,341 notes... done (14s)
Use the new slash commands and resource type:
/vault-search kubernetes networking notes
@vault://Projects/Q3-planning.md Summarize the key decisions
What Just Happened?
After install, Gemini CLI's session startup automatically launched the Obsidian MCP server in the background. The /vault-search slash command calls that server's search_notes tool. The @vault:// resource prefix is registered by the extension as a custom resource handler — it resolves to the file contents of the matching vault note. None of this required manual JSON configuration.
What an Extension Adds to Your Session
| Contribution | Example | Registered As |
|---|---|---|
| Slash commands | /vault-search, /pr-review | Skill .md files → skill registry |
| @ resource types | @vault://note-title | Custom resource handler in MCP server |
| Tools | create_issue, search_notes | MCP server tool declarations |
| System context | Domain knowledge, preferences | Merged into context stack via gemini.md |
Extension Anatomy
File Structure
Every extension follows a predictable directory layout. The CLI looks for each component at a known path — nothing is discovered dynamically.
The package.json gemini.extension Config Block
The gemini.extension key in package.json is what the CLI reads to understand what the extension provides. Every field is optional except name.
{
"name": "gemini-ext-github-pr",
"version": "1.2.0",
"description": "GitHub PR review and issue triage skills for Gemini CLI",
"gemini.extension": {
"displayName": "GitHub PR Assistant",
"description": "AI-powered PR review, descriptions, and issue triage via GitHub API",
"icon": "assets/icon.png",
"mcpServers": [
{
"name": "github-pr-mcp",
"command": "node",
"args": ["mcp/server.js"],
"env": {
"GITHUB_TOKEN": "${env:GITHUB_TOKEN}"
}
}
],
"skills": [
"skills/pr-review.md",
"skills/pr-description.md",
"skills/issue-triage.md"
],
"contextFile": "gemini.md",
"permissions": ["network", "env:GITHUB_TOKEN"]
}
}
mcpServers tells the CLI how to start the bundled MCP server. The ${env:GITHUB_TOKEN} syntax safely injects environment variables without hardcoding secrets.
skills is an array of relative paths to .md files. Each file becomes a slash command whose name matches the filename (e.g., pr-review.md → /pr-review).
permissions array is displayed to the user during install — clearly declaring required permissions (especially env:* and network) prevents install-time rejections and builds user trust.
Build: GitHub PR Assistant Extension
This walkthrough builds a complete, working extension. By the end you'll have three slash commands (/pr-review, /pr-description, /issue-triage) backed by a lightweight MCP server that calls the GitHub REST API.
Step 1 — Write the Skill Files
Skill files are plain Markdown. The YAML frontmatter declares the command name and description; the body is the prompt template Gemini executes when the command is invoked.
---
name: pr-review
description: Review a GitHub PR for correctness, security, and test coverage
arguments:
- name: pr_number
description: GitHub PR number (e.g. 142)
required: true
---
You are a senior engineer performing a thorough code review.
Use the `get_pull_request` tool to fetch PR #{{pr_number}} including the diff,
title, description, and list of changed files.
Review the changes for:
1. **Correctness** — logic errors, off-by-one bugs, null pointer risks
2. **Security** — OWASP Top 10, injection risks, hardcoded secrets
3. **Breaking changes** — API surface changes, database migrations
4. **Test coverage** — are new code paths exercised by tests?
Output your review as structured Markdown with a summary section first,
then per-file findings. Use GitHub Flavored Markdown so the output can be
pasted directly as a PR comment.
arguments array defines what the user can pass when invoking the command: /pr-review 142. Arguments are injected as Mustache-style {{pr_number}} tokens.
---
name: issue-triage
description: Classify a GitHub issue and suggest labels + response
arguments:
- name: issue_number
description: Issue number to triage
required: true
---
Use `get_issue` to fetch issue #{{issue_number}}.
Classify it as one of: bug / feature-request / question / documentation / duplicate.
Then:
- Suggest the appropriate GitHub labels (e.g., "bug", "enhancement", "needs-info")
- If the issue lacks steps to reproduce, draft a clarifying comment asking for them
- If it's a duplicate, identify the likely original issue number if possible
- Assign a priority: P0 (blocking), P1 (high), P2 (normal), P3 (low)
Output the classification, labels, priority, and any draft comment as JSON
so the CLI can apply them automatically via the GitHub API.
Step 2 — Write the MCP Server
The MCP server exposes the GitHub API as tools that Gemini can call. It's a Node.js process that communicates over stdioStandard input/output — the simplest MCP server transport. The CLI launches the server as a child process and sends JSON-RPC messages over stdin/stdout. No network ports needed..
// mcp/server.ts — GitHub PR Assistant MCP server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
console.error("GITHUB_TOKEN environment variable is required");
process.exit(1);
}
// ── Tool definitions ──────────────────────────────────────────────────
const TOOLS = [
{
name: "get_pull_request",
description: "Fetch a GitHub pull request including diff and file list",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
pr_number: { type: "number", description: "Pull request number" },
},
required: ["owner", "repo", "pr_number"],
},
},
{
name: "get_issue",
description: "Fetch a GitHub issue by number",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
issue_number: { type: "number" },
},
required: ["owner", "repo", "issue_number"],
},
},
{
name: "post_pr_comment",
description: "Post a comment on a pull request",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
pr_number: { type: "number" },
body: { type: "string", description: "Markdown comment body" },
},
required: ["owner", "repo", "pr_number", "body"],
},
},
];
// ── GitHub API helper ─────────────────────────────────────────────────
async function githubFetch(path: string, options: RequestInit = {}) {
const res = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "gemini-ext-github-pr/1.0",
...((options.headers as Record) || {}),
},
});
if (!res.ok) {
throw new Error(`GitHub API error ${res.status}: ${await res.text()}`);
}
return res.json();
}
// ── Server setup ──────────────────────────────────────────────────────
const server = new Server(
{ name: "github-pr-mcp", version: "1.2.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "get_pull_request") {
const { owner, repo, pr_number } = args as any;
const [pr, diff] = await Promise.all([
githubFetch(`/repos/${owner}/${repo}/pulls/${pr_number}`),
githubFetch(`/repos/${owner}/${repo}/pulls/${pr_number}/files`),
]);
return {
content: [{ type: "text", text: JSON.stringify({ pr, files: diff }) }],
};
}
if (name === "get_issue") {
const { owner, repo, issue_number } = args as any;
const issue = await githubFetch(
`/repos/${owner}/${repo}/issues/${issue_number}`
);
return { content: [{ type: "text", text: JSON.stringify(issue) }] };
}
if (name === "post_pr_comment") {
const { owner, repo, pr_number, body } = args as any;
await githubFetch(
`/repos/${owner}/${repo}/issues/${pr_number}/comments`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body }),
}
);
return { content: [{ type: "text", text: "Comment posted successfully." }] };
}
throw new Error(`Unknown tool: ${name}`);
} catch (err: any) {
return {
content: [{ type: "text", text: `Error: ${err.message}` }],
isError: true,
};
}
});
// ── Start server ──────────────────────────────────────────────────────
const transport = new StdioServerTransport();
await server.connect(transport);
# mcp/server.py — GitHub PR Assistant MCP server (Python variant)
import asyncio
import json
import os
import sys
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
if not GITHUB_TOKEN:
print("GITHUB_TOKEN environment variable is required", file=sys.stderr)
sys.exit(1)
HEADERS = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "gemini-ext-github-pr/1.0",
}
server = Server("github-pr-mcp")
# ── Tool definitions ──────────────────────────────────────────────────
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="get_pull_request",
description="Fetch a GitHub PR including diff and file list",
inputSchema={
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"pr_number": {"type": "integer"},
},
"required": ["owner", "repo", "pr_number"],
},
),
types.Tool(
name="get_issue",
description="Fetch a GitHub issue by number",
inputSchema={
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"issue_number": {"type": "integer"},
},
"required": ["owner", "repo", "issue_number"],
},
),
types.Tool(
name="post_pr_comment",
description="Post a Markdown comment on a pull request",
inputSchema={
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"pr_number": {"type": "integer"},
"body": {"type": "string"},
},
"required": ["owner", "repo", "pr_number", "body"],
},
),
]
# ── Tool implementations ──────────────────────────────────────────────
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
async with httpx.AsyncClient() as client:
try:
if name == "get_pull_request":
owner, repo, pr_num = (
arguments["owner"],
arguments["repo"],
arguments["pr_number"],
)
pr_resp = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_num}",
headers=HEADERS,
)
pr_resp.raise_for_status()
files_resp = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_num}/files",
headers=HEADERS,
)
files_resp.raise_for_status()
result = {"pr": pr_resp.json(), "files": files_resp.json()}
return [types.TextContent(type="text", text=json.dumps(result))]
elif name == "get_issue":
owner, repo, issue_num = (
arguments["owner"],
arguments["repo"],
arguments["issue_number"],
)
resp = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_num}",
headers=HEADERS,
)
resp.raise_for_status()
return [types.TextContent(type="text", text=json.dumps(resp.json()))]
elif name == "post_pr_comment":
owner, repo, pr_num, body = (
arguments["owner"],
arguments["repo"],
arguments["pr_number"],
arguments["body"],
)
resp = await client.post(
f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_num}/comments",
headers={**HEADERS, "Content-Type": "application/json"},
json={"body": body},
)
resp.raise_for_status()
return [types.TextContent(type="text", text="Comment posted successfully.")]
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
return [types.TextContent(type="text", text=f"Error: {e}")]
# ── Start server ──────────────────────────────────────────────────────
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
/help. The inputSchema is standard JSON Schema — the MCP SDK validates inputs against it automatically.
isError: true on failure. This lets Gemini decide whether to retry, ask for clarification, or surface the error to the user — rather than crashing the server.
What Just Happened?
You now have a complete extension: three skill files that give the CLI /pr-review, /pr-description, and /issue-triage commands, and an MCP server that executes real GitHub API calls when Gemini invokes those tools. The skill files contain the reasoning instructions; the MCP server handles the I/O. This is the clean separation that makes extensions composable.
Testing Your Extension Locally
# Install from local path during development
gemini extensions install ./my-extension --dev
# Verify the extension is loaded
gemini extensions list
# Output:
# gemini-ext-github-pr v1.2.0 (dev) 3 skills 1 MCP server
# Test a skill
$env:GITHUB_TOKEN = "ghp_yourtoken"
gemini
# > /pr-review 142
# > /issue-triage 56
# Install from local path during development
gemini extensions install ./my-extension --dev
# Verify the extension is loaded
gemini extensions list
# Output:
# gemini-ext-github-pr v1.2.0 (dev) 3 skills 1 MCP server
# Test a skill
export GITHUB_TOKEN="ghp_yourtoken"
gemini
# > /pr-review 142
# > /issue-triage 56
Publishing Your Extension
Once your extension works locally, publishing it makes it installable by anyone with a gemini extensions install <url> command.
When you publish a VS Code extension, you're not moving your code — you're registering a pointer in the marketplace that tells others "this GitHub repo contains a valid extension." The pain without a registry is that sharing means emailing a Git URL and hoping the other person knows how to install it. The mapping: gemini extensions publish is the equivalent of running vsce publish — it submits your repo URL, name, and README to the geminicli.com gallery so others can find it by keyword search.
# From your extension root directory
cd my-extension
# Validate package.json structure before submitting
gemini extensions validate
# Publish to geminicli.com gallery
# (Requires GitHub auth — opens browser for OAuth)
gemini extensions publish
# Output:
# Validating package.json... OK
# Checking README.md... OK (1,240 words)
# Submitting to gallery...
# Published: https://geminicli.com/extensions/gemini-ext-github-pr
# Install: gemini extensions install https://github.com/yourname/gemini-ext-github-pr
# From your extension root directory
cd my-extension
# Validate package.json structure before submitting
gemini extensions validate
# Publish to geminicli.com gallery
gemini extensions publish
# Output:
# Validating package.json... OK
# Checking README.md... OK (1,240 words)
# Submitting to gallery...
# Published: https://geminicli.com/extensions/gemini-ext-github-pr
# Install: gemini extensions install https://github.com/yourname/gemini-ext-github-pr
The gallery renders your README.md directly. A complete README should include: a one-paragraph description, the list of slash commands the extension adds, required environment variables (e.g., GITHUB_TOKEN), an install command, and a usage example for each major command. Extensions without a README fail validation.
Community Extensions Worth Installing
The awesome-gemini-cli-extensionsA community-curated GitHub repo (similar to awesome-* lists) that catalogs high-quality Gemini CLI extensions with descriptions, install commands, and ratings. Maintained by community contributors. list is the fastest way to find extensions for your domain. Here are the standouts:
RAG over your Obsidian vault. Full-text + semantic search, backlink traversal, note creation. Eliminates manual copy-paste from notes to prompts.
KnowledgeNetwork analysis: parse pcap files, explain packet flows, diagnose latency issues. Adds /packet-analyze and /subnet-calc.
Natural language queries over PostgreSQL. Schema-aware — loads your schema into context so Gemini generates correct SQL on the first try.
DatabaseSprint summaries, ticket creation, backlog triage. Reads your sprint board and generates standup updates or retrospective notes.
Project MgmtKubernetes cluster assistant. /pod-debug, /resource-audit, and /helm-review commands with live cluster access via kubectl.
Stripe payments debugging. Fetch payment intents, explain webhook events, and draft Stripe API integration code against live API responses.
PaymentsA developer without the Obsidian extension spends 5-10 minutes per session manually finding relevant notes and pasting them into prompts. With the extension, /vault-search kubernetes networking finds and injects the 3 most relevant notes in 2 seconds. Across a 40-hour work week, that's 2-4 hours recovered. The Packet Buddy extension eliminates the need to export pcap files, open Wireshark, and manually describe packet flows — the extension does all three steps in one command.
Section Complete — Extensions
You know what extensions are, how to install them, their file anatomy, and how to build and publish one. You've also seen the MCP server pattern that powers every extension's real-world tool calls. The next module extends this into CI/CD pipelines with GitHub Actions.
Knowledge Check
Five questions to confirm the key concepts from this module.
1. When you run gemini extensions install <url>, where does the CLI clone the extension to?
.gemini/extensions/~/.gemini/extensions/2. Which file in an extension's directory is merged into the Gemini CLI session's context stack?
README.mdpackage.jsongemini.mdmcp/server.js3. What determines the slash command name for a skill file? For example, what slash command does skills/pr-review.md create?
name field in the gemini.extension block of package.jsonname field in the skill file's YAML frontmatter (and the filename as default)4. You're building an extension that connects to an internal company API. Which field in package.json's gemini.extension block should you use to declare the required API token environment variable?
secretsenv inside mcpServers only — no top-level declaration neededpermissions (e.g., "env:COMPANY_API_TOKEN") so it's shown to the user at install time5. What is the security risk of running gemini extensions install without reviewing the extension source?
~/.gemini/GEMINI.md global config silently