<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Caleb Mintoumba - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Caleb Mintoumba - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 29 Jul 2026 22:31:35 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/phoekerson/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Firestore Data Modeling Guide: Embedded Documents vs Referencing (with a Blog Case Study) ]]>
                </title>
                <description>
                    <![CDATA[ When developers transition from the relational world (MySQL, PostgreSQL) to Firestore, Firebase's NoSQL document database, they often bring their old habits with them. They try to replicate tables, fo ]]>
                </description>
                <link>https://www.freecodecamp.org/news/firestore-data-modeling-guide-embedded-documents-vs-referencing-with-a-blog-case-study/</link>
                <guid isPermaLink="false">6a63826ed2f5d140f2aaa325</guid>
                
                    <category>
                        <![CDATA[ firestore ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firebase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NoSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Query ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Caleb Mintoumba ]]>
                </dc:creator>
                <pubDate>Fri, 24 Jul 2026 15:19:10 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f0166ca3-ca48-45f6-bb2f-ee6b20701ea0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When developers transition from the relational world (MySQL, PostgreSQL) to Firestore, Firebase's NoSQL document database, they often bring their old habits with them. They try to replicate tables, foreign keys, and joins.</p>
<p>The result? Complex queries, skyrocketing read costs, and a database structure that becomes a nightmare to maintain after just a few features.</p>
<p>To understand how Firestore works, we first need to look at our point of comparison: the relational model. Once we map out how SQL does things, we can see exactly where Firestore diverges, and how to structure NoSQL data correctly.</p>
<p>In this guide, we'll cover NoSQL design principles, embedding vs. referencing, and relationship modeling (1-1, 1-N, N-N). We'll also walk through a concrete blog case study.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-relational-mindset-how-sql-handles-data">The Relational Mindset: How SQL Handles Data</a></p>
</li>
<li><p><a href="#heading-the-firestore-paradigm-nosql-with-relationships">The Firestore Paradigm: NoSQL with Relationships</a></p>
</li>
<li><p><a href="#heading-the-core-building-blocks-documents-and-collections">The Core Building Blocks: Documents and Collections</a></p>
</li>
<li><p><a href="#heading-the-golden-rule-model-for-reads-not-writes">The Golden Rule: Model for Reads, Not Writes</a></p>
</li>
<li><p><a href="#heading-embedding-vs-referencing-denormalization">Embedding vs. Referencing (Denormalization)</a></p>
</li>
<li><p><a href="#heading-how-to-model-relationships-1-1-1-n-n-n">How to Model Relationships (1-1, 1-N, N-N)</a></p>
</li>
<li><p><a href="#heading-best-practices-and-pitfalls-to-avoid">Best Practices and Pitfalls to Avoid</a></p>
</li>
<li><p><a href="#heading-case-study-designing-a-scalable-blog-database">Case Study: Designing a Scalable Blog Database</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This guide is conceptual, so you don't need a running Firestore project to follow along. A little context is enough. You will need:</p>
<ul>
<li><p>Basic JavaScript syntax, since every code example uses the modular Firebase JS SDK (v9+)</p>
</li>
<li><p>Basic familiarity with JSON objects (keys, values, nested objects, arrays)</p>
</li>
<li><p>Some exposure to SQL or relational databases helps, since the guide leans on that comparison throughout (but it's not required)</p>
</li>
<li><p>(Optional) A free Firebase project, if you want to try the examples yourself. The <a href="https://firebase.google.com/docs/firestore/quickstart">Firestore quickstart</a> walks you through setting one up.</p>
</li>
</ul>
<p>No prior NoSQL or Firestore experience is needed.</p>
<h2 id="heading-the-relational-mindset-how-sql-handles-data">The Relational Mindset: How SQL Handles Data</h2>
<p>In a relational database, data is organized into tables linked by explicit relationships. This approach relies on <strong>normalization</strong> to eliminate data redundancy.</p>
<p>For example, to store users and their respective countries, we split the data into two tables:</p>
<ul>
<li><p><code>Users</code>: columns <code>id</code> (PK), <code>last_name</code>, <code>first_name</code>, <code>#country_id</code> (FK a foreign key)</p>
</li>
<li><p><code>Countries</code>: columns <code>country_id</code> (PK), <code>country_name</code></p>
</li>
</ul>
<p>With a row like <code>1, MINTOUMBA, Caleb, 1</code> in <code>Users</code> and <code>1, Canada</code> in <code>Countries</code>, we automatically know that Caleb belongs to Canada through the foreign key <code>#country_id</code>. We never had to write the word "Canada" inside the <code>Users</code> table itself.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/ac10a248-0b5e-4be7-a738-3a6bdd54c1d7.png" alt="Relational model showing a Users table linked to a Countries table through a foreign key" style="display:block;margin:0 auto" width="2179" height="1019" loading="lazy">

<p><strong>The SQL trade-off:</strong> writes are lightweight (you only update data in one place), but reads are heavier, because you have to perform a database join (<code>JOIN</code>) every time you want to display a user's country name.</p>
<p>That's exactly the opposite of how Firestore works, as we'll see next.</p>
<h2 id="heading-the-firestore-paradigm-nosql-with-relationships">The Firestore Paradigm: NoSQL with Relationships</h2>
<p>Firestore is a <strong>NoSQL</strong> document database – literally <em>Not Only SQL</em>. It stores JSON-like documents grouped into collections, with no enforced schema.</p>
<p>For most of Firestore's history, that also meant no native joins and no <code>GROUP BY</code>. The standard query engine simply didn't support them, and any aggregation beyond <code>count()</code>, <code>sum()</code>, and <code>average()</code> had to happen in your application code.</p>
<p>That's still true today for <strong>Standard edition</strong>, which remains the default and the one most mobile/web apps run on and the one this guide focuses on.</p>
<p>Google has since introduced <strong>Firestore Enterprise edition</strong>, built around a new <strong>Pipeline</strong> query engine that reached general availability in April 2026. Pipelines add a multi-stage query syntax and hundreds of new functions, including relational-style joins through correlated subqueries and a real <code>aggregate(...)</code> step with grouping Firestore's equivalent of SQL's <code>GROUP BY</code>.</p>
<p><strong>Does this mean data modeling doesn't matter anymore?</strong> Not for most apps. Pipeline queries run within a 60-second timeout and a 128 MiB working-memory limit, can fall back to full collection scans when no index exists, and critically, Enterprise edition drops real-time listeners and offline support (which most Firestore client apps depend on).</p>
<p>Pipelines are a genuine escape hatch for analytical, admin, or reporting queries. They're not a drop-in replacement for the read-optimized structure your app's everyday screens still need.</p>
<p>If you're building a typical client-facing app on Standard edition, the embedding and denormalization strategies below are still how you model relationships.</p>
<p>But <strong>NoSQL doesn't mean "no relationships"</strong> even on Standard edition. You can and should build robust relationships between your collections. The difference is that Firestore won't enforce or resolve them for you the way a <code>JOIN</code> does by default. It's up to you, the developer, to build and query those relationships explicitly, and to maintain data integrity through your application code or Cloud Functions unless you've specifically opted into Enterprise edition for Pipeline-powered joins.</p>
<h2 id="heading-the-core-building-blocks-documents-and-collections">The Core Building Blocks: Documents and Collections</h2>
<p>Before designing any schema, let's define Firestore's two core building blocks:</p>
<ul>
<li><p><strong>Document</strong>: the basic unit of storage. It's a JSON-like object, identified by a unique ID, containing typed fields (strings, numbers, booleans, timestamps, geopoints, or references to other documents).</p>
</li>
<li><p><strong>Collection</strong>: a container for documents. Unlike SQL tables, documents in the same collection don't need to share the same structure.</p>
</li>
</ul>
<p>What makes Firestore unique is its hierarchical nature: <strong>a document can contain sub-collections</strong>, which contain more documents, which can themselves contain more sub-collections, and so on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/3d49ff84-8648-4c15-bcc3-60caeb540a77.png" alt="Firestore hierarchy diagram showing a posts collection containing the post_001 document, which holds a comments sub-collection with individual comment documents" style="display:block;margin:0 auto" width="2179" height="1259" loading="lazy">

<p>In the diagram above, the root <code>posts</code> collection contains the document <code>post_001</code>, which itself hosts a <code>comments</code> sub-collection containing the individual comment documents <code>comment_001</code> and <code>comment_002</code>. You can nest collections and documents several levels deep, but as we'll see later, it's best to do so sparingly.</p>
<p><strong>Crucial rule:</strong> sub-collections are never retrieved automatically when you read a parent document. Unlike a SQL <code>JOIN</code>, you must always perform a separate, explicit query to read a sub-collection.</p>
<h2 id="heading-the-golden-rule-model-for-reads-not-writes">The Golden Rule: Model for Reads, Not Writes</h2>
<p>This is the single most important concept in NoSQL modeling, and the one developers coming from SQL forget most often: <strong>structure your data based on how your app queries it, not on how it gets written.</strong></p>
<p>Before writing any database code, ask yourself:</p>
<ul>
<li><p>Which screens in my app will display this data?</p>
</li>
<li><p>Do I need this piece of data on its own, or always alongside another one?</p>
</li>
<li><p>Do I read this information significantly more often than I write or update it?</p>
</li>
</ul>
<p>If your users view a writer's profile 10,000 times for every single time that writer updates their username, optimize for the reads: duplicate the username directly inside each post. That's the exact opposite of the SQL instinct we saw earlier, where you normalize first to avoid redundancy, even if it makes reads heavier.</p>
<h2 id="heading-embedding-vs-referencing-denormalization">Embedding vs. Referencing (Denormalization)</h2>
<p>There are two primary strategies for representing a relationship in Firestore.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/ac6d31d1-9c75-4688-b39a-e01ffa55ea07.png" alt="Side-by-side comparison of embedding comments directly inside a post document versus referencing them through a separate comments sub-collection" style="display:block;margin:0 auto" width="2179" height="1180" loading="lazy">

<h3 id="heading-option-a-embedding-nesting">Option A: Embedding (Nesting)</h3>
<p>You store the related data directly inside the parent document, as an array or a map (object).</p>
<pre><code class="language-js">// A post with its comments embedded
{
  title: "Introduction to Firestore",
  author: "Caleb",
  comments: [
    { user: "Ama", text: "Great post!" },
    { user: "Kofi", text: "Thanks for the examples" }
  ]
}
</code></pre>
<ul>
<li><p><strong>Pros</strong>: a single read retrieves everything, and consistency is guaranteed.</p>
</li>
<li><p><strong>Cons</strong>: Firestore documents have a hard <strong>1 MB size limit</strong>. If the nested list grows indefinitely (comments on a viral post, for instance), your writes will start failing once you hit that limit and every write to the parent document also re-sends the whole document to any client listening in real time.</p>
</li>
<li><p><strong>Best for</strong>: small, bounded lists (tags on an article, a user's settings, a short list of favorites).</p>
</li>
</ul>
<h3 id="heading-option-b-referencing-denormalization">Option B: Referencing (Denormalization)</h3>
<p>You split the entities into separate collections or sub-collections, and deliberately duplicate a few fields to avoid a second read.</p>
<pre><code class="language-js">// posts/post_001
{
  title: "Introduction to Firestore",
  authorId: "uid_123",
  authorName: "Caleb",      // denormalized: avoids a second read to "users"
  authorAvatar: "https://...",
  commentCount: 12          // denormalized counter
}

// posts/post_001/comments/comment_001
{
  userId: "uid_456",
  userName: "Ama",
  text: "Great post!",
  createdAt: Timestamp
}
</code></pre>
<p>Here, we duplicate the author's name and avatar into every post so we don't need an extra read to <code>users</code> every time the post list is displayed.</p>
<p>That's denormalization: we accept controlled redundancy in exchange for faster reads the exact opposite of SQL normalization. The cost is that these copies need updating if the user changes their name (usually handled by a Cloud Function triggered when the <code>users</code> document is updated).</p>
<ul>
<li><p><strong>Pros</strong>: no document size limits, and entities can be queried independently.</p>
</li>
<li><p><strong>Cons</strong>: requires multiple reads if you didn't denormalize enough. If a duplicated value changes, you need code (often a Cloud Function) to propagate the update everywhere it's copied.</p>
</li>
<li><p><strong>Best for</strong>: dynamic, fast-growing data (comments, order history, activity logs).</p>
</li>
</ul>
<p><strong>A more precise rule of thumb</strong>: whether to <em>reference instead of embed</em> depends on volume. Sub-collections handle unbounded growth (comments, order history) better than arrays.</p>
<p>Whether to <em>denormalize a given field</em> depends on the cost of keeping it in sync, not how often it changes: a counter you update in place with an atomic increment (<code>commentCount</code>, <code>likeCount</code>) has no other copy to synchronize, so it's cheap to denormalize regardless of frequency.</p>
<p>A copied value like <code>authorName</code>, on the other hand, is duplicated across every document that references it. It's safe to denormalize only if it changes rarely, since any change means propagating the update everywhere it's been copied.</p>
<h2 id="heading-how-to-model-relationships-1-1-1-n-n-n">How to Model Relationships (1-1, 1-N, N-N)</h2>
<h3 id="heading-one-to-one-1-1">One-to-One (1-1)</h3>
<p>Either embed the fields in the same document, or store them in a separate collection using the exact same document ID, for example <code>users/uid_123</code> and <code>privateProfiles/uid_123</code>. This is perfect for separating public data from sensitive data that needs different security rules.</p>
<h3 id="heading-one-to-many-1-n">One-to-Many (1-N)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/cc6057e0-f3c9-42dd-827b-4546033b6248.png" alt="One-to-many relationship diagram showing a post document linked to multiple comment documents through a sub-collection" style="display:block;margin:0 auto" width="2179" height="980" loading="lazy">

<p>There are three main options, depending on volume and query direction:</p>
<ol>
<li><p>A <strong>sub-collection</strong> (<code>posts/post_001/comments/*</code>) is ideal when you almost always query comments <em>through</em> their parent post, and volume can be large.</p>
</li>
<li><p>A <strong>root collection with a reference</strong> (<code>comments</code> with a <code>postId</code> field) is useful if you also need to query all comments by a given user, independently of the post (<code>where("userId", "==", uid)</code>).</p>
</li>
<li><p>Use an <strong>embedded array</strong> only if the volume stays small and bounded (see Option A above).</p>
</li>
</ol>
<h3 id="heading-many-to-many-n-n">Many-to-Many (N-N)</h3>
<p>This is the trickiest one in NoSQL, since there's no automatic join table like in SQL. There are three common patterns:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/b8b96fab-5180-43fb-98fd-fe16faac162f.png" alt="Many-to-many relationship diagram showing a memberships junction collection linking users and groups" style="display:block;margin:0 auto" width="2179" height="1060" loading="lazy">

<p><strong>(1). Junction collection</strong> the equivalent of a SQL pivot table:</p>
<pre><code class="language-js">// memberships/{membershipId}
{
  userId: "uid_123",
  groupId: "group_789",
  role: "admin",
  joinedAt: Timestamp
}
</code></pre>
<p>You can then query <code>.where("userId", "==", uid)</code> to find all groups a user belongs to, or <code>.where("groupId", "==", gid)</code> to find all members of a group.</p>
<p><strong>(2). ID arrays on both sides</strong> (cross-denormalization):</p>
<pre><code class="language-js">// users/uid_123      -&gt; groupIds: ["group_789", "group_456"]
// groups/group_789   -&gt; memberIds: ["uid_123", "uid_456"]
</code></pre>
<p>Fast to read from either side, but reserve this for lists that stay small the 1 MB document limit and the cost of atomically updating long arrays both work against you at scale.</p>
<p><strong>(3). Hybrid approach</strong>, which is the most common pattern in practice: an array for a lightweight relationship rarely queried from the other side (a user's favorite posts), and a junction collection for a relationship queried frequently in both directions and prone to frequent changes (team memberships).</p>
<h2 id="heading-best-practices-and-pitfalls-to-avoid">Best Practices and Pitfalls to Avoid</h2>
<ul>
<li><p><strong>Limit nesting depth:</strong> Firestore allows sub-collections to be nested indefinitely, but beyond two or three levels, your queries and security rules become genuinely hard to maintain. Prefer flattening the structure with references when you can.</p>
</li>
<li><p><strong>Avoid auto-incremented document IDs:</strong> Sequential IDs (<code>user_1</code>, <code>user_2</code>, <code>user_3</code>...) can cause <em>hotspotting</em>: writes pile up on a narrow range of the index, which degrades performance at scale. Let Firestore generate random, evenly distributed IDs unless you have a specific reason not to.</p>
</li>
<li><p><strong>Watch out for composite indexes:</strong> Any query combining multiple <code>.where()</code> filters, or a <code>.where()</code> with an <code>.orderBy()</code> on a different field, requires a composite index. Plan for these during design rather than discovering them in production (Firestore's error messages include a direct link to auto-generate the missing index).</p>
</li>
<li><p><strong>Mind the write rate on "hot" documents:</strong> The recommended maximum <em>sustained</em> write rate to a single document is about <strong>1 write per second</strong>. A document updated very frequently by many different users a global like counter, for example becomes a bottleneck well before that. Firestore can absorb short bursts (5, 10, even 50 writes in one second) by queuing them, but sustained traffic above ~1 write/sec will start producing contention errors. The standard fix is a <em>sharded counter</em>: split the count across several sub-documents and sum them at read time.</p>
</li>
<li><p><strong>Use sub-collections deliberately:</strong> They're convenient, but they always require a separate query. If you almost always need the data together, embedding or denormalization will perform better.</p>
</li>
<li><p><strong>Design security rules alongside your data model:</strong> Firestore's security rules (<code>firestore.rules</code>) should be designed at the same time as your schema a poorly thought-out structure usually makes precise rules much harder to write.</p>
</li>
</ul>
<h2 id="heading-case-study-designing-a-scalable-blog-database">Case Study: Designing a Scalable Blog Database</h2>
<p>Let's bring every principle from this guide together with a concrete example: a blog with posts, comments, and likes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/d774e41d-28d6-4d0a-81cb-a7bd088992d9.png" alt="Complete Firestore schema for a blog application showing the posts, comments sub-collection, and likes collection" style="display:block;margin:0 auto" width="2379" height="1300" loading="lazy">

<pre><code class="language-js">// posts/{postId}
{
  title: "Modeling Firestore",
  slug: "modeling-firestore",
  authorId: "uid_123",
  authorName: "Caleb",         // denormalized: avoids a second read to "users"
  content: "...",
  tags: ["firebase", "nosql"], // embedded: small, bounded list
  commentCount: 3,             // denormalized counter
  likeCount: 47,               // denormalized counter (shard it if traffic is high)
  createdAt: Timestamp
}

// posts/{postId}/comments/{commentId}  → sub-collection: read together with the post
{
  userId: "uid_456",
  userName: "Ama",
  text: "Excellent article",
  createdAt: Timestamp
}

// likes/{likeId}  → root collection + reference
{                    // lets you quickly check if ONE user liked ONE post
  postId: "post_001",
  userId: "uid_456"
}
</code></pre>
<p>Each choice here answers a specific read pattern. Tags are always displayed alongside the post, so they're embedded. Comments can grow large in number and are almost always fetched together with their parent post, so they live in a sub-collection. Likes need to be queried both by post <em>and</em> by user to check whether <em>this</em> user already liked <em>this</em> post so they sit in a root collection with two indexable fields.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In SQL, you normalize to eliminate redundancy, and you pay for that choice at read time, via joins. In Firestore, it's the opposite: you accept controlled redundancy (denormalization) to make reads instant and cheap, at the cost of slightly heavier writes.</p>
<p>Modeling data in Firestore isn't about applying relational habits with a different syntax. It's a genuinely different way of thinking, centered on your app's read patterns.</p>
<p>Always ask "how will I read this data, and how often?" before choosing between embedding, referencing, or a sub-collection. Also, keep Firestore's concrete limits in mind (1 MB per document, composite indexes, hotspotting) from the design phase rather than discovering them in production.</p>
<p>That balance between read simplicity and write cost is what separates a Firestore database that scales gracefully from one you'll be rewriting six months from now.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Agentic Terminal Workflow with GitHub Copilot CLI and MCP Servers ]]>
                </title>
                <description>
                    <![CDATA[ Most developers live in their terminal. You run commands, debug pipelines, manage infrastructure, and navigate codebases, all from a shell prompt. But despite how central the terminal is to developer  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-agentic-terminal-workflow-with-github-copilot-cli-and-mcp-servers/</link>
                <guid isPermaLink="false">69f212526e0124c05e1857b5</guid>
                
                    <category>
                        <![CDATA[ Developer Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ terminal ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Caleb Mintoumba ]]>
                </dc:creator>
                <pubDate>Wed, 29 Apr 2026 14:14:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3e4e3d7e-6cbf-4742-a63b-f9a2579f2318.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most developers live in their terminal. You run commands, debug pipelines, manage infrastructure, and navigate codebases, all from a shell prompt.</p>
<p>But despite how central the terminal is to developer workflows, AI assistance there has remained shallow: autocomplete a command here, explain an error there.</p>
<p>That changes when you combine GitHub Copilot CLI with MCP (Model Context Protocol) servers. Instead of an AI that reacts to isolated prompts, you get a terminal that understands your project context, queries live data sources, and chains tool calls autonomously – what the industry is starting to call an agentic workflow.</p>
<p>In this tutorial, you'll learn exactly how to wire these two systems together, step by step. By the end, your terminal will be able to do things like understand your Git history before suggesting a fix, query your running Docker containers before writing a compose patch, or pull live API schemas before generating a request.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-github-copilot-cli">What is GitHub Copilot CLI?</a></p>
</li>
<li><p><a href="#heading-what-is-the-model-context-protocol">What is the Model Context Protocol?</a></p>
</li>
<li><p><a href="#heading-how-mcp-servers-work-in-a-terminal-context">How MCP Servers Work in a Terminal Context</a></p>
</li>
<li><p><a href="#heading-step-1-install-and-configure-github-copilot-cli">Step 1 – Install and Configure GitHub Copilot CLI</a></p>
</li>
<li><p><a href="#heading-step-2-set-up-your-first-mcp-server">Step 2 – Set Up Your First MCP Server</a></p>
</li>
<li><p><a href="#heading-step-3-wire-copilot-cli-to-your-mcp-server">Step 3 – Wire Copilot CLI to Your MCP Server</a></p>
</li>
<li><p><a href="#heading-step-4-build-a-real-agentic-workflow">Step 4 – Build a Real Agentic Workflow</a></p>
</li>
<li><p><a href="#heading-step-5-extend-with-multiple-mcp-servers">Step 5 – Extend with Multiple MCP Servers</a></p>
</li>
<li><p><a href="#heading-debugging-common-issues">Debugging Common Issues</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before you start, make sure you have the following:</p>
<ul>
<li><p><strong>Node.js</strong> v18 or later (<code>node --version</code>)</p>
</li>
<li><p><strong>npm</strong> v9 or later</p>
</li>
<li><p>A GitHub account with Copilot enabled. The free tier (available to all GitHub users) is sufficient to follow this tutorial. Pro, Business, and Enterprise plans unlock higher usage limits but aren't required.</p>
</li>
<li><p><strong>GitHub CLI</strong> (<code>gh</code>) installed. We'll use it to authenticate.</p>
</li>
<li><p>Basic familiarity with the terminal and JSON configuration files</p>
</li>
<li><p>(Optional) <strong>Docker</strong> installed if you want to follow the Docker MCP example in Step 5</p>
</li>
</ul>
<p>You don't need prior experience with MCP or agentic AI systems, as this guide builds that understanding from the ground up.</p>
<h2 id="heading-what-is-github-copilot-cli">What is GitHub Copilot CLI?</h2>
<p>GitHub Copilot CLI is the terminal-native interface to GitHub's Copilot AI. Unlike the IDE plugin (which assists with code completion), Copilot CLI is designed specifically for shell workflows. It exposes three main commands:</p>
<ul>
<li><p><code>gh copilot suggest</code> proposes a shell command based on a natural language description</p>
</li>
<li><p><code>gh copilot explain</code> explains what a given command does</p>
</li>
<li><p><code>gh copilot alias</code> generates shell aliases for Copilot subcommands</p>
</li>
</ul>
<p>Here's a quick example of <code>suggest</code> in action:</p>
<pre><code class="language-shell">gh copilot suggest "find all files modified in the last 24 hours and larger than 1MB"
</code></pre>
<p>Copilot will return something like:</p>
<pre><code class="language-shell">find . -mtime -1 -size +1M
</code></pre>
<p>It will also ask if you want to copy it, run it directly, or revise the request. This interactive loop is already useful – but by itself, Copilot CLI has no awareness of your project context. It doesn't know your repo structure, your running services, or your deployment environment. That's where MCP comes in.</p>
<h2 id="heading-what-is-the-model-context-protocol">What is the Model Context Protocol?</h2>
<p>The <strong>Model Context Protocol (MCP)</strong> is an open standard introduced by Anthropic in late 2024. Its goal is straightforward: give AI models a standardized way to connect to external tools, data sources, and services.</p>
<p>Think of MCP as a universal adapter layer between an AI model and the real world. Without MCP, each AI integration is custom-built: one plugin for GitHub, another for Postgres, another for Slack, all with incompatible interfaces. MCP defines a single protocol that any tool can implement, and any compatible AI client can consume.</p>
<p>An MCP server exposes <strong>tools</strong> (functions the AI can call), <strong>resources</strong> (data the AI can read), and <strong>prompts</strong> (reusable instruction templates). The AI client in our case, a Copilot-powered terminal discovers these capabilities at runtime and uses them autonomously to complete a task.</p>
<p>A few notable MCP servers that are already production-ready:</p>
<table>
<thead>
<tr>
<th>MCP Server</th>
<th>What it exposes</th>
</tr>
</thead>
<tbody><tr>
<td>@modelcontextprotocol/server-filesystem</td>
<td>Read/write access to local files</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-git</td>
<td>Git log, diff, blame, branch operations</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-github</td>
<td>GitHub Issues, PRs, repos via API</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-postgres</td>
<td>Live query execution on a Postgres DB</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-docker</td>
<td>Container inspection, logs, stats</td>
</tr>
</tbody></table>
<p>The full registry lives at <code>github.com/modelcontextprotocol/servers</code>.</p>
<h2 id="heading-how-mcp-servers-work-in-a-terminal-context">How MCP Servers Work in a Terminal Context</h2>
<p>Before we get hands-on, it's worth understanding the communication model.</p>
<p>MCP servers run as local processes. They communicate with the AI client over <strong>stdio</strong> (standard input/output) or over an <strong>HTTP/SSE transport</strong>. The client sends JSON-RPC messages to the server, and the server responds with structured data.</p>
<p>Here's the simplified flow:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/e1844dc5-a869-4201-ad8a-fd1cb305f646.png" alt="An architectural flowchart illustrating the Model Context Protocol (MCP) workflow. The process starts with a user typing a natural language prompt, passes through the Copilot CLI (the MCP client), communicates via JSON-RPC over stdio with an MCP Server (e.g., server-git), executes real tools like git log, returns a structured result to Copilot, which finally synthesizes a context-aware response for the user." style="display:block;margin:0 auto" width="1408" height="768" loading="lazy">

<p>The key word here is <strong>grounded</strong>. Without MCP, Copilot responds based purely on its training data and your prompt. With MCP, it can call <code>git log --oneline -20</code> before answering your question about recent regressions and its answer is based on <em>your actual code history</em>, not a generalized assumption.</p>
<h3 id="heading-step-1-install-and-configure-github-copilot-cli">Step 1 – Install and Configure GitHub Copilot CLI</h3>
<p>If you haven't already, install the GitHub CLI:</p>
<pre><code class="language-shell"># macOS
brew install gh

# Ubuntu/Debian
sudo apt install gh

# Windows (via winget)
winget install --id GitHub.cli
</code></pre>
<p>Then authenticate:</p>
<pre><code class="language-shell">gh auth login
</code></pre>
<p>Follow the interactive prompts. Select <strong>GitHub.com</strong>, then <strong>HTTPS</strong>, and authenticate via browser when prompted.</p>
<p>Now install the Copilot CLI extension:</p>
<pre><code class="language-shell">gh extension install github/gh-copilot
</code></pre>
<p>Verify the installation:</p>
<pre><code class="language-shell">gh copilot --version
</code></pre>
<p>You should see output like <code>gh-copilot version 1.x.x</code>.</p>
<p><strong>Optional but recommended: set up shell aliases.</strong> This makes the workflow much faster. For <code>bash</code> or <code>zsh</code>:</p>
<pre><code class="language-shell"># Add to your ~/.bashrc or ~/.zshrc
eval "$(gh copilot alias -- bash)"   # for bash
eval "$(gh copilot alias -- zsh)"    # for zsh
</code></pre>
<p>After reloading your shell (<code>source ~/.bashrc</code>), you can use <code>ghcs</code> as shorthand for <code>gh copilot suggest</code> and <code>ghce</code> for <code>gh copilot explain</code>.</p>
<h3 id="heading-step-2-set-up-your-first-mcp-server">Step 2 – Set Up Your First MCP Server</h3>
<p>We'll start with <code>server-git</code>. It's the most immediately useful for a development workflow and has zero external dependencies.</p>
<p>Install it globally via npm:</p>
<pre><code class="language-shell">npm install -g @modelcontextprotocol/server-git
</code></pre>
<p>Test that it runs:</p>
<pre><code class="language-shell">mcp-server-git --version
</code></pre>
<p>This server exposes the following tools to any compatible MCP client:</p>
<ul>
<li><p><code>git_log</code> retrieve commit history with filters</p>
</li>
<li><p><code>git_diff</code> diff between branches or commits</p>
</li>
<li><p><code>git_status</code> current working tree status</p>
</li>
<li><p><code>git_show</code> inspect a specific commit</p>
</li>
<li><p><code>git_blame</code> annotate file lines with commit info</p>
</li>
<li><p><code>git_branch</code> list or switch branches</p>
</li>
</ul>
<p>Now create a configuration file. MCP clients look for a file called <code>mcp.json</code> to discover available servers. Create it in your project root or in a global config directory:</p>
<pre><code class="language-shell">mkdir -p ~/.config/mcp
touch ~/.config/mcp/mcp.json
</code></pre>
<p>Add the following content:</p>
<pre><code class="language-markdown">{
  "mcpServers": {
    "git": {
      "command": "mcp-server-git",
      "args": ["--repository", "."],
      "transport": "stdio"
    }
  }
}
</code></pre>
<p>A few notes on this config:</p>
<ul>
<li><p><code>command</code> is the binary to run. Make sure it's on your <code>$PATH</code>.</p>
</li>
<li><p><code>args</code> passes <code>--repository .</code> so the server scopes itself to the current working directory.</p>
</li>
<li><p><code>transport: "stdio"</code> means communication happens over standard input/output the simplest and most stable option for local servers.</p>
</li>
</ul>
<h3 id="heading-step-3-wire-copilot-cli-to-your-mcp-server">Step 3 – Wire Copilot CLI to Your MCP Server</h3>
<p>This is where the two systems connect. GitHub Copilot CLI supports MCP via its <code>--mcp-config</code> flag (available from version 1.3+). You point it at your <code>mcp.json</code>, and Copilot will automatically initialize the declared servers before processing your prompt.</p>
<p>Here's the basic invocation:</p>
<pre><code class="language-shell">gh copilot suggest --mcp-config ~/.config/mcp/mcp.json "why did the build break in the last commit?"
</code></pre>
<p>When you run this inside a Git repository, Copilot CLI will:</p>
<ol>
<li><p>Start the <code>mcp-server-git</code> process</p>
</li>
<li><p>Call <code>git_log</code> to retrieve recent commits</p>
</li>
<li><p>Call <code>git_diff</code> on the most recent commit</p>
</li>
<li><p>Synthesize an answer based on the actual diff output</p>
</li>
</ol>
<p>Try it yourself on a repo with a recent failing commit. The difference in response quality compared to a plain <code>gh copilot suggest</code> is immediately obvious.</p>
<p><strong>Tip: avoid retyping the flag every time.</strong> Add a shell function to your <code>.bashrc</code>/<code>.zshrc</code>:</p>
<pre><code class="language-shell">function aterm() {
  gh copilot suggest --mcp-config ~/.config/mcp/mcp.json "$@"
}
</code></pre>
<p>Now you just type:</p>
<pre><code class="language-shell">aterm "what changed between main and feature/auth?"
</code></pre>
<p>And you're running a fully context-aware, MCP-powered query from a single short command. This function name <code>aterm</code> for <em>agentic terminal</em> is what we'll use throughout the rest of this tutorial.</p>
<h3 id="heading-step-4-build-a-real-agentic-workflow">Step 4 – Build a Real Agentic Workflow</h3>
<p>Let's move beyond individual queries and build a workflow that chains multiple tool calls to complete a real developer task: <strong>diagnosing a regression</strong>.</p>
<p>Imagine you pushed a feature branch and your CI pipeline failed. You don't know exactly which change caused it. Here's how your agentic terminal handles it:</p>
<h4 id="heading-query-1-understand-what-changed">Query 1: understand what changed</h4>
<pre><code class="language-shell">aterm "summarize all commits on feature/auth that aren't on main yet"
</code></pre>
<p>Copilot calls <code>git_log</code> with branch filters, then returns a structured summary of commits unique to your branch. No copy-pasting SHAs manually.</p>
<h4 id="heading-query-2-isolate-the-diff">Query 2: isolate the diff</h4>
<pre><code class="language-shell">aterm "show me everything that changed in the auth middleware between main and feature/auth"
</code></pre>
<p>This triggers <code>git_diff</code> scoped to the path containing your middleware. Copilot returns the diff with an explanation of what each change does.</p>
<h4 id="heading-query-3-find-the-likely-culprit">Query 3: find the likely culprit</h4>
<pre><code class="language-shell">aterm "which of those changes could cause a JWT validation failure?"
</code></pre>
<p>At this point, Copilot has the diff in its context window from the previous tool calls. It reasons over the actual code changes not generic knowledge about JWT and pinpoints the likely issue.</p>
<h4 id="heading-query-4-generate-the-fix">Query 4: generate the fix</h4>
<pre><code class="language-shell">aterm "write the corrected version of that validation function"
</code></pre>
<p>Copilot generates a targeted fix based on the specific code it retrieved via MCP. You get a patch you can directly apply, not a generic code template.</p>
<p>This four-step sequence – understand, isolate, reason, fix – is a complete agentic loop. Each step is grounded in live repository data retrieved through MCP tools. The AI is not hallucinating context. Instead, it's reading your actual codebase.</p>
<h3 id="heading-step-5-extend-with-multiple-mcp-servers">Step 5 – Extend with Multiple MCP Servers</h3>
<p>One MCP server is useful. Multiple MCP servers working together is where the workflow becomes genuinely powerful. Let's add two more: <code>server-filesystem</code> and <code>server-docker</code>.</p>
<p>Install the additional servers:</p>
<pre><code class="language-shell">npm install -g @modelcontextprotocol/server-filesystem
npm install -g @modelcontextprotocol/server-docker
</code></pre>
<p>Update your <code>mcp.json</code>:</p>
<pre><code class="language-markdown">{
  "mcpServers": {
    "git": {
      "command": "mcp-server-git",
      "args": ["--repository", "."],
      "transport": "stdio"
    },
    "filesystem": {
      "command": "mcp-server-filesystem",
      "args": ["--root", "."],
      "transport": "stdio"
    },
    "docker": {
      "command": "mcp-server-docker",
      "transport": "stdio"
    }
  }
}
</code></pre>
<p>With all three servers active, your terminal can now answer cross-domain questions:</p>
<pre><code class="language-shell">aterm "my Express app container keeps restarting, check the logs and compare with what the healthcheck in my Dockerfile expects"
</code></pre>
<p>To answer this, Copilot will:</p>
<ol>
<li><p>Call <code>docker_logs</code> (server-docker) to pull the container's recent stderr output</p>
</li>
<li><p>Call <code>read_file</code> (server-filesystem) to read your <code>Dockerfile</code></p>
</li>
<li><p>Parse the <code>HEALTHCHECK</code> instruction</p>
</li>
<li><p>Cross-reference the log errors with the health endpoint path</p>
</li>
<li><p>Return a diagnosis explaining the mismatch and suggest the fix</p>
</li>
</ol>
<p>This is an <strong>agentic workflow</strong>: the model autonomously decides which tools to call, in what order, and synthesizes the results into a coherent answer. You didn't tell it to read the Dockerfile. It inferred that was necessary based on your question.</p>
<p><strong>A note on security:</strong> When running <code>server-filesystem</code>, always scope it to a specific directory using <code>--root</code>. Never point it at <code>/</code> or your home directory. Similarly, <code>server-docker</code> has access to your Docker socket run it only in trusted environments.</p>
<h2 id="heading-debugging-common-issues">Debugging Common Issues</h2>
<p><code>mcp-server-git: command not found</code></p>
<p>The npm global bin directory isn't on your <code>$PATH</code>. Fix:</p>
<pre><code class="language-shell">export PATH="\(PATH:\)(npm bin -g)"
# or for newer npm versions:
export PATH="\(PATH:\)(npm prefix -g)/bin"
</code></pre>
<p>Add this line to your <code>.bashrc</code>/<code>.zshrc</code> to persist it.</p>
<h4 id="heading-copilot-cli-doesnt-seem-to-be-using-mcp-tools">Copilot CLI doesn't seem to be using MCP tools</h4>
<p>Check your Copilot CLI version:</p>
<pre><code class="language-shell">gh copilot --version
</code></pre>
<p>MCP support requires version 1.3 or later. Update with:</p>
<pre><code class="language-shell">gh extension upgrade copilot
</code></pre>
<p>Also verify your <code>mcp.json</code> is valid JSON a trailing comma or missing bracket will silently prevent server initialization.</p>
<h4 id="heading-mcp-server-starts-but-returns-no-data">MCP server starts but returns no data</h4>
<p>Run the server manually to check for errors:</p>
<pre><code class="language-shell">mcp-server-git --repository .
</code></pre>
<p>If it exits immediately, check that you're running the command inside a valid Git repository. For <code>server-docker</code>, make sure the Docker daemon is running and your user has access to the Docker socket:</p>
<pre><code class="language-shell">sudo usermod -aG docker $USER
# Then log out and back in
</code></pre>
<h4 id="heading-responses-are-slow-with-multiple-servers">Responses are slow with multiple servers</h4>
<p>Each MCP server is a separate subprocess. Spawning several at once adds startup latency, especially on slower machines. Two optimizations:</p>
<ol>
<li><p>Only declare the servers you actually need for a given project in your <code>mcp.json</code></p>
</li>
<li><p>Use project-specific config files instead of one global config:</p>
</li>
</ol>
<pre><code class="language-shell"># project A (backend)
aterm --mcp-config ./mcp-backend.json "..."

# project B (infra)
aterm --mcp-config ./mcp-infra.json "..."
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've just built an agentic terminal workflow from scratch. Here's a quick recap of what you did:</p>
<ul>
<li><p>Installed and configured GitHub Copilot CLI with shell aliases for fast access</p>
</li>
<li><p>Set up MCP servers (<code>server-git</code>, <code>server-filesystem</code>, <code>server-docker</code>) and wired them through a <code>mcp.json</code> config</p>
</li>
<li><p>Created a shell function (<code>aterm</code>) that transparently passes your MCP config to every Copilot query</p>
</li>
<li><p>Built a multi-step agentic loop for diagnosing regressions using live Git data</p>
</li>
<li><p>Extended the setup with cross-domain tool orchestration across Git, filesystem, and Docker</p>
</li>
</ul>
<p>The architecture you've built here is not a demo – it's a production-ready pattern. You can extend it with any MCP-compatible server: <code>server-postgres</code> for database-aware queries, <code>server-github</code> for issue and PR context, or custom MCP servers you write yourself for your internal APIs.</p>
<p>The terminal has always been the most powerful surface in a developer's environment. With Copilot CLI and MCP, it's finally becoming an intelligent one.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
