<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ana Jimenez Santamaria</title>
    <description>The latest articles on DEV Community by Ana Jimenez Santamaria (@anajsana95).</description>
    <link>https://dev.to/anajsana95</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F344556%2Ff87b2a04-5b88-47b8-949f-a5bd5154f2df.JPG</url>
      <title>DEV Community: Ana Jimenez Santamaria</title>
      <link>https://dev.to/anajsana95</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anajsana95"/>
    <language>en</language>
    <item>
      <title>Leafy Goose: A Human-Led AI Assistant for Maintainers’ Community Work</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:28:33 +0000</pubDate>
      <link>https://dev.to/anajsana95/leafy-goose-a-human-led-ai-assistant-for-maintainers-community-work-64f</link>
      <guid>https://dev.to/anajsana95/leafy-goose-a-human-led-ai-assistant-for-maintainers-community-work-64f</guid>
      <description>&lt;p&gt;&lt;strong&gt;How I built a human-led community assistant with Goose Agent and Skill.md&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every tech community knows this moment: a newcomer joins, says hi, and asks &lt;em&gt;"What should I do first? Please help!"&lt;/em&gt; Welcoming one newcomer is easy. Welcoming new people every single week, with the same questions, is a different story.&lt;/p&gt;

&lt;p&gt;In a Discord open source community I help with, the same "how do I get started?" questions come back again and again: &lt;em&gt;Do I need to attend every meeting? Where do I share my idea?&lt;/em&gt; The answers are already in our guides. But I get it: people want a quick answer in the place where they asked. They don't have time to dig through pages, and they want to feel there is a person on the other side.&lt;/p&gt;

&lt;p&gt;Most moderators from this community are volunteers, so those answers sometimes wait days. A bot that answers on its own is not the fix either. It is fast, but it loses the human connection, and it can be wrong. So I kept thinking: &lt;strong&gt;how can we answer faster, right where people ask, and still keep a human in the loop?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My answer this time was a build with &lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;goose&lt;/a&gt;, a SKILL.md and Discord. I started with a small bot called Goose Bot v1 and ended with &lt;strong&gt;Leafy Goose&lt;/strong&gt; 🍃🪿, an open source bot where AI drafts the answer and a human always decides what gets posted.&lt;/p&gt;

&lt;p&gt;In this post, I share how I got there, step by step + a link to the public repo at the end to test it yourself with your community&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the workflow once, reuse it anywhere
&lt;/h2&gt;

&lt;p&gt;I wanted to build a reusable workflow that other trusted community members could help review and maintain. I also wanted to adapt it to other communities I’m involved with by changing the resources goose reads, without touching the code.&lt;/p&gt;

&lt;p&gt;That led me to use goose as the AI layer behind the Discord bot. The bot handles the Discord interaction, while goose uses the community’s resources and instructions to draft answers to questions phrased in different ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why goose over a direct API call?
&lt;/h3&gt;

&lt;p&gt;This is a fair question I got from folks who have been following my work. I could have connected the bot directly to a model API. None of the building blocks below is unique to goose, but goose gives me all of them in one open source tool, ready to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider changes without code changes:&lt;/strong&gt; the model provider is configured in goose's settings, so switching providers doesn't require editing the bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable instructions:&lt;/strong&gt; the same &lt;code&gt;SKILL.md&lt;/code&gt; can be used outside Discord, making the workflow useful beyond the bot (more on this in step 2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools when needed:&lt;/strong&gt; extensions add capabilities as the workflow grows. For example, a calendar integration could let the bot check when the next community call is scheduled, something I could add in the future.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Today the bot is built for Discord, so other chat tools would need some extra work. But only one part of the architecture would change: the chat layer. The guide, goose and the answer checks can stay the same.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Leafy Goose has one rule: &lt;strong&gt;AI drafts, a human publishes.&lt;/strong&gt; A reviewer (a moderator, maintainer or community manager) picks a member's question. The bot asks goose for a draft, using only the community guide. The reviewer reads the private draft and decides.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhzu6m6fv1kumbj80cvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhzu6m6fv1kumbj80cvs.png" alt="Leafy Goose architecture" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only the prompt goes out to the model provider, and only a reviewer's click puts an answer in a public channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You need four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js 20 or later&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://goose-docs.ai/docs/getting-started/installation" rel="noopener noreferrer"&gt;goose CLI&lt;/a&gt;&lt;/strong&gt; signed in to a model provider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Discord app&lt;/strong&gt; from the &lt;a href="https://discord.com/developers/applications" rel="noopener noreferrer"&gt;Developer Portal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage Server&lt;/strong&gt; permission on your Discord server, so you can add the bot. Only the person who installs it needs this. Other reviewers don't need it (the person who runs the bot adds their Discord user IDs to the settings)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. The prototype
&lt;/h2&gt;

&lt;p&gt;My first bot was simple. A slash command took the question, sent it to goose, and showed the answer. It worked and taught me fast what can go wrong when a model talks to real people:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acronyms are ambiguous: the community name is an acronym, and the model at first answered about a popular web framework with the same initials&lt;/li&gt;
&lt;li&gt;The model has its own style and that lack of customization was odd in a community channel.&lt;/li&gt;
&lt;li&gt;There are old links, meeting times, channels that are private that the model cannot tell which facts are still true unless you give them proper instructions &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these became one rule in the next version&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdv2c9go61enzdzv8szm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdv2c9go61enzdzv8szm.png" alt=" " width="799" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. One guide, one source of truth
&lt;/h2&gt;

&lt;p&gt;All the facts live in one Markdown file: the community guide. Changing what the bot says means editing that file, not the code.&lt;/p&gt;

&lt;p&gt;The guide has a few simple rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public information only. The bot may quote anything in it, so no passwords, private links or personal data.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Last reviewed: &amp;lt;date&amp;gt;&lt;/code&gt; line. Drafts show a warning when the guide is older than 30 days.&lt;/li&gt;
&lt;li&gt;Every link in the guide is an approved link, and every &lt;code&gt;&amp;lt;#channel&amp;gt;&lt;/code&gt; mention is an approved public channel. The bot builds its allowlists from this file (more on that in step 5).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first it was a plain &lt;code&gt;.md&lt;/code&gt; file. Later I turned it into a &lt;strong&gt;&lt;a href="https://goose-docs.ai/docs/guides/context-engineering/using-skills" rel="noopener noreferrer"&gt;goose skill&lt;/a&gt;&lt;/strong&gt;, so the same guide could also works beyond the bot if more people want to make use of it (more in the side story below).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drf-community-guide&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Approved&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;facts,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;links&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Discord&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;channels&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;community.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;answers&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;about&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;getting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;started,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;calls,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;events,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;contributing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conduct."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Plain .md vs SKILL.md
&lt;/h3&gt;

&lt;p&gt;With a plain &lt;code&gt;.md&lt;/code&gt;, an agent does not know the file exists: you have to paste it or ask the agent to read it every time. A skill is found automatically: the agent sees its name and description in every session, and loads the full guide only when a question matches or when you ask for it.&lt;/p&gt;

&lt;p&gt;So why does the bot not load it as a skill? goose loads a skill only when it decides the question matches the description. For a bot that must always stay on facts, "maybe" is not good enough. The bot reads the file itself, removes the top block, and puts the whole guide in every prompt&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🪿 &lt;strong&gt;Side story: a skill beyond the bot.&lt;/strong&gt; Because the guide is a standard &lt;code&gt;SKILL.md&lt;/code&gt;, it is useful outside Discord too. Community managers, moderators, or anyone curious about the community can add it to their favorite AI agent (goose, or any other agent that supports skills). Then they can ask it for help with a welcome post, an answer in GitHub Discussions or a newsletter. Same facts and links, wherever they work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Keeping a human-review in every answer
&lt;/h2&gt;

&lt;p&gt;This is the heart of Leafy Goose. Every answer starts as an ephemeral message: only the reviewer who asked can see it. Under the draft there are three buttons: &lt;strong&gt;Publish&lt;/strong&gt;, &lt;strong&gt;Edit&lt;/strong&gt; and &lt;strong&gt;Discard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A reviewer can start a draft by right-click a member's message, then &lt;strong&gt;Apps&lt;/strong&gt;, then &lt;strong&gt;Answer with Leafy Goose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsilukhoufs9d3xwgi4n6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsilukhoufs9d3xwgi4n6.png" alt=" " width="688" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvswfbjegyutsamjxh8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvswfbjegyutsamjxh8g.png" alt=" " width="455" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Discord wants a first response within 3 seconds, and goose needs longer. So the bot defers the reply first, then asks goose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...allowlist, cooldown and "one request at a time" checks...&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deferReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ephemeral&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;guideLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;askGoose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GOOSE_TIMEOUT_MS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guideLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guide&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`🤔 Leafy Goose has no approved answer for this (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noAnswerReason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;drafts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expireDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;DRAFT_TTL_MS&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 14 minutes&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;renderDraft&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;draftButtons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drafts expire after 14 minutes, because Discord interaction tokens last 15.&lt;/p&gt;

&lt;p&gt;This is what a reviewer sees. Only they can read it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zh6pfgwhqkbsub2ax6b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zh6pfgwhqkbsub2ax6b.png" alt=" " width="652" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the reviewer clicks Publish, the bot replies &lt;strong&gt;to the member's message&lt;/strong&gt; when there is one. That way the answer stays next to the question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few small details that matter in a real community:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The draft is deleted from memory &lt;strong&gt;before&lt;/strong&gt; posting, so a double click cannot post twice.&lt;/li&gt;
&lt;li&gt;Only the reviewer who made a draft can press its buttons or open its edit form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowedMentions&lt;/code&gt; blocks &lt;code&gt;@everyone&lt;/code&gt;, &lt;code&gt;@here&lt;/code&gt; and role pings. Only the member being answered is mentioned.&lt;/li&gt;
&lt;li&gt;Link previews are off, so one answer does not fill the channel with cards.&lt;/li&gt;
&lt;li&gt;Every published answer ends with a small footer: &lt;em&gt;AI-assisted answer, reviewed by &lt;a class="mentioned-user" href="https://dev.to/reviewer"&gt;@reviewer&lt;/a&gt;.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuiu3j8ztqt8gtyqcctd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuiu3j8ztqt8gtyqcctd.png" alt=" " width="473" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Running Goose
&lt;/h2&gt;

&lt;p&gt;goose is an agent: with extensions it can run shell commands and read files. For a Discord bot I want the opposite. goose should read one prompt and write one answer, nothing more.&lt;/p&gt;

&lt;p&gt;The bot calls the goose CLI with &lt;code&gt;execFile&lt;/code&gt; (no shell) and four flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--quiet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--max-turns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execFileAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOSE_BIN&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;goose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeGooseEnv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;safeGooseEnv()&lt;/code&gt; does two jobs. It sets &lt;code&gt;GOOSE_MODE=chat&lt;/code&gt;, so goose does not call tools. And it removes every &lt;code&gt;DISCORD_*&lt;/code&gt; variable, so the Discord token never reaches goose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeGooseEnv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;GOOSE_MODE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DISCORD_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt has two clearly marked parts: the guide, which the bot trusts, and the member's message, which it doesn't. Member text could try a prompt injection to ignore the rules, reveal the prompt or act as something else. No single trick stops that, so the bot uses layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It escapes every &lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&lt;/code&gt; in the message, so it can't close its marker and pretend to be part of the guide (code below)&lt;/li&gt;
&lt;li&gt;The prompt tells goose to treat the message as untrusted and to reply &lt;code&gt;NO_ANSWER: off-topic request&lt;/code&gt; to these attempts&lt;/li&gt;
&lt;li&gt;A human reviews every draft before anything is posted
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_QUESTION_CHARS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt rules are short and strict: always English, at most 120 words, one clear next step, only links from the guide, never a date for a future meeting. When the guide has no answer, the model replies with one line: &lt;code&gt;NO_ANSWER: &amp;lt;reason&amp;gt;&lt;/code&gt;. The bot then tells the reviewer to answer by hand, instead of guessing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Watch your error logs:&lt;/strong&gt; One of the things I did to avoid leaking members' questions was to never log goose's raw error. When goose fails, Node's error repeats the full command, question included. Leafy Goose only reports &lt;code&gt;goose failed (exit code 1)&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before a draft reaches the reviewer, the bot also flags links or channels that are not in the guide, meta comments and very long answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tests
&lt;/h2&gt;

&lt;p&gt;There are two kinds of tests in the repo, and they answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Unit tests check the code: is the guide loaded? are bad links flagged? do Discord secrets stay out of the goose environment? They use a fake guide and goose script, so they need no Discord, no goose and no network&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evals check the answers: Each eval is a real question, plus words the answer must or must not contain:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"how can i contribuite to drf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mustInclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"touchpoint"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mustNotInclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"if you mean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contribuite"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one checks two things. The answer points to a community call, and the bot does not comment on the member's typo. Other evals check that the bot never invents a date, never links a private channel, answers in English, and replies &lt;code&gt;NO_ANSWER&lt;/code&gt; to off-topic questions and injection attempts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2brjctcbweco3d3xyz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2brjctcbweco3d3xyz8.png" alt=" " width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Set it up for your community
&lt;/h2&gt;

&lt;p&gt;Leafy Goose started as a bot for one community, but nothing in the code is specific to it. To use it in another Discord community, you write a new guide, change a few settings and update your own &lt;code&gt;.env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One running bot serves all your reviewers: add their Discord user IDs to &lt;code&gt;DISCORD_ALLOWED_USER_ID&lt;/code&gt;. The code is at &lt;a href="https://github.com/anajsana/leafy-goose" rel="noopener noreferrer"&gt;github.com/anajsana/leafy-goose&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final remarks
&lt;/h2&gt;

&lt;p&gt;As a maintainer in open source communities, I know the community load first-hand, and there is still a lot of talk about AI adding to maintainer burnout. However, this time, with Leafy Goose, I wanted to show the other side: the side where AI can take on the repetitive and boring part of community work, while maintainers keep the voice, the judgment and the human connection.&lt;/p&gt;

&lt;p&gt;I also wanted another community manager to be able to take this, point goose to their own community's guide, and review the answers without having to rebuild the Discord bot. If you try it in your community, I'd love to hear how it goes! 👋&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>Faster Model, Better Agent? Training Efficient AI Agents</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:27:20 +0000</pubDate>
      <link>https://dev.to/anajsana95/faster-model-better-agent-training-efficient-ai-agents-3p48</link>
      <guid>https://dev.to/anajsana95/faster-model-better-agent-training-efficient-ai-agents-3p48</guid>
      <description>&lt;p&gt;&lt;strong&gt;Notes from PyTorch Meetups Madrid, and what OpenEnv means for engineering teams&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first-ever &lt;a href="https://pytorch.org/event/pytorch-meetups-madrid-spain/2026-10-14/" rel="noopener noreferrer"&gt;PyTorch Meetup Madrid&lt;/a&gt; was &lt;a href="https://www.youtube.com/watch?v=wo3azAq0eao&amp;amp;t=3689s" rel="noopener noreferrer"&gt;last week&lt;/a&gt;! Ricardo Noriega (PyTorch Foundation Ambassador) and I (AAIF Ambassador) opened the event, after us, Carlos Camacho (Red Hat) talked about serving agents at scale wiht d-llm, and Sergio Paniego Blanco (Hugging Face) talked about training them wiht OpenEnv.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: While I'd love to write a full post about all the meetup discussions and sessions, this one will focus on the OpenEnv presentation. You can &lt;a href="https://www.youtube.com/watch?v=wo3azAq0eao&amp;amp;t=3689s" rel="noopener noreferrer"&gt;watch the full recording here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A small model that solved more tasks and used fewer tools
&lt;/h2&gt;

&lt;p&gt;I came home thinking about one result from &lt;a href="https://youtu.be/wo3azAq0eao?si=g3EK_onexYyzNbBi&amp;amp;t=3745" rel="noopener noreferrer"&gt;Sergio's talk&lt;/a&gt;: a cool experiment where a small model, after training, solved more tasks and used fewer tools to do it. Most conversations I hear about agents are about which model is fastest, but this one was about how much work the agent does before it finishes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9edql55h4wp0y20horzd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9edql55h4wp0y20horzd.jpg" alt=" " width="799" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The training setup used &lt;a href="https://github.com/huggingface/OpenEnv" rel="noopener noreferrer"&gt;OpenEnv&lt;/a&gt;, which has just joined the &lt;a href="https://aaif.io" rel="noopener noreferrer"&gt;Agentic AI Foundation (AAIF)&lt;/a&gt;. I didn't really understand the value OpenEnv could bring before the meetup, but I ended up testing it myself after the session. This post is a short intro to OpenEnv and how it connects with the broader ecosystem, such as MCP and Goose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training an agent is different from training a model
&lt;/h2&gt;

&lt;p&gt;Ask a coding agent to fix a bug. It reads a file, edits it, runs the tests, sees a failure and tries again. Writing code that looks right is only part of the job. The rest is deciding what to do after each result.&lt;/p&gt;

&lt;p&gt;Sergio covered three ways to train for that: learning from recordings of agents doing the task (supervised fine-tuning), learning from a stronger teacher model (distillation), and learning from scores on the model's own attempts (reinforcement learning). Hugging Face's post-training curriculum explains all three.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffr91746879pnet3nx1zf.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffr91746879pnet3nx1zf.jpeg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx5ip7tx4xc2dn16mm973.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx5ip7tx4xc2dn16mm973.jpeg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The third one needs somewhere for the model to practice. That's an environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, what is OpenEnv exactly?
&lt;/h2&gt;

&lt;p&gt;In practice, an &lt;a href="https://huggingface.co/docs/openenv/tutorials/openenv-tutorial" rel="noopener noreferrer"&gt;OpenEnv environment&lt;/a&gt; is like a small server, usually running in a Docker container. Inside it you put three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The task:&lt;/strong&gt; for example, fixing a bug in a Python function that should apply a discount to a shopping basket total (but doesn't)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tools the agent can use:&lt;/strong&gt; like reading a file, editing it and running the tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The code that checks the result and gives a score.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your agent sits outside and talks to it through three calls: &lt;code&gt;reset()&lt;/code&gt; to start a new attempt, &lt;code&gt;step()&lt;/code&gt; to take an action, and &lt;code&gt;state()&lt;/code&gt; to see where things are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CallToolAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;OpenEnv doesn't decide what counts as a good answer (you write that). What it gives you is a standard shape, so the same environment works with different agents and different training libraries, like Hugging Face's &lt;a href="https://huggingface.co/docs/trl" rel="noopener noreferrer"&gt;TRL&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How can an environment like OpenEnv help with agentic AI adoption?
&lt;/h2&gt;

&lt;p&gt;With everything we've seen so far, you might be thinking: "OK, but my team doesn't train models" (and most of the teams, don't!) The good news is you can still use &lt;em&gt;an environment&lt;/em&gt; to test and measure your agents.&lt;/p&gt;

&lt;p&gt;And that matters, because many teams are stuck between a prototype or demo and a production-ready agent they can trust. Last year, &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;Gartner predicted&lt;/a&gt; that over 40% of agentic AI projects will be canceled by the end of 2027, because of rising costs, unclear business value or weak risk controls.&lt;/p&gt;

&lt;p&gt;Here's how an environment can help with three of the most common challenges engineering teams face.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement: "Is our agent &lt;em&gt;actually&lt;/em&gt; good at our tasks?"
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://www.langchain.com/state-of-agent-engineering" rel="noopener noreferrer"&gt;LangChain's survey&lt;/a&gt; of more than 1,300 professionals, only about 52% of teams run evaluations, while nearly 89% have observability. In other words, most teams can see what their agent did, but fewer can tell if it did it well.&lt;/p&gt;

&lt;p&gt;With an environment, you get numbers: how many tasks the agent solved and how many steps it needed, across different models and providers. It works like a test suite, but for your agent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Over three-quarters of organizations use more than one model in production or development. Maybe you want a cheaper model, a local one, or the new release everyone is talking about. Today most teams try it for a few days and go by feel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Cost and speed: "Our agent gets there, but it takes forever"
&lt;/h3&gt;

&lt;p&gt;Every extra tool call costs time, tokens and money. Sergio's result is about exactly this. In his experiment, LFM2.5-2.6B went from solving about 42% to 54% of tasks it had never seen, averaged across four harnesses. On tasks the base model already solved, it used 31% fewer tool calls. And while fewer calls and fewer tokens aren't the same thing, it shows that efficiency can be measured and improved, not only the final answer. Even before any training, an environment records every step, so you can see where your agent wastes time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Risk: "We can't let an agent experiment on our real systems"
&lt;/h3&gt;

&lt;p&gt;Part of the problem is that the only way to really know how an agent behaves is to let it act. And nobody wants it learning on the production side, right?&lt;/p&gt;

&lt;p&gt;An environment can be a safe copy: a fake calendar, a copy of a repo, a test database. The agent can fail as many times as it needs without touching anything real.&lt;/p&gt;

&lt;p&gt;A real example: Turing built &lt;a href="https://huggingface.co/blog/openenv-turing" rel="noopener noreferrer"&gt;Calendar Gym&lt;/a&gt; on OpenEnv to test how agents schedule meetings: which calendar to use, what they have permission to change, and which actions must happen first. According to the &lt;a href="https://aaif.io/blog/training-agents-for-real-world-tasks-with-openenv" rel="noopener noreferrer"&gt;OpenEnv maintainers&lt;/a&gt;, agents succeeded about 90% of the time when tasks named explicit calendar IDs, but only about 40% when the same tasks used natural-language descriptions.&lt;/p&gt;

&lt;p&gt;That's the kind of thing you want to discover in a sandbox, not in production!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Quick note about security. An environment doesn't make your agent secure in production. That's a different job, and it's where projects like &lt;a href="https://agentgateway.dev" rel="noopener noreferrer"&gt;agentgateway&lt;/a&gt; come in, with policies on what an agent can access&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The connection with MCP
&lt;/h2&gt;

&lt;p&gt;Some OpenEnv environments expose their tools through MCP, so any MCP-compatible agent can use them. &lt;a href="https://huggingface.co/docs/openenv/tutorials/mcp-environment" rel="noopener noreferrer"&gt;The docs&lt;/a&gt; explain how this works.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenEnv is now part of AAIF
&lt;/h2&gt;

&lt;p&gt;Since the meetup, &lt;a href="https://www.linuxfoundation.org/press/agentic-ai-foundation-announces-four-new-projects-agent-skills-open-responses-openenv-and-opensandbox" rel="noopener noreferrer"&gt;AAIF announced four new projects&lt;/a&gt;: Agent Skills, Open Responses, OpenEnv and OpenSandbox. Hugging Face contributed OpenEnv, which it &lt;a href="https://huggingface.co/blog/openenv" rel="noopener noreferrer"&gt;launched with Meta&lt;/a&gt; in 2025.&lt;/p&gt;

&lt;p&gt;For me, this closes the loop of our first meetup. The PyTorch side brings training and serving, many of the AAIF projects bring the open standards agents use to work (MCP, Goose, AGENTS.md), and I'd say OpenEnv sits in the middle, where you test and train agents on the tasks you actually care about.&lt;/p&gt;

&lt;p&gt;Testing it myself helped me understand all of this much better than reading about it. Next, I want to build a small environment from real tasks and see what it tells me about an agent like &lt;a href="https://goose-docs.ai/" rel="noopener noreferrer"&gt;Goose&lt;/a&gt;. I'll share it here when I do!&lt;/p&gt;

&lt;p&gt;Thanks to Ricardo, Carlos, Sergio, Red Hat and everyone who came! Watch the &lt;a href="https://www.youtube.com/watch?v=wo3azAq0eao&amp;amp;t=3689s" rel="noopener noreferrer"&gt;full recording&lt;/a&gt; (in Spanish) and follow &lt;a href="https://www.linkedin.com/company/pytorch-meetups-madrid/" rel="noopener noreferrer"&gt;PyTorch Meetups Madrid&lt;/a&gt; for future events.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>aiops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Taking my GitHub Stats MCP Server to the Cloud 🚀☁️</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Sun, 28 Jun 2026 17:45:44 +0000</pubDate>
      <link>https://dev.to/anajsana95/taking-my-github-stats-mcp-server-to-the-cloud-4m0b</link>
      <guid>https://dev.to/anajsana95/taking-my-github-stats-mcp-server-to-the-cloud-4m0b</guid>
      <description>&lt;p&gt;&lt;em&gt;👋 This is the third chapter of a series where I document what I'm learning about Model Context Protocol architecture and tool implementations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/anajsana95/building-a-github-stats-mcp-server-with-security-metrics-3omi"&gt;Chapter 2&lt;/a&gt;, I built a GitHub Stats MCP server with &lt;a href="https://chaoss.community/practitioner-guide-security/" rel="noopener noreferrer"&gt;CHAOSS security metrics&lt;/a&gt;. It worked, it returned real data, and &lt;a href="https://goose-docs.ai/" rel="noopener noreferrer"&gt;Goose&lt;/a&gt; could orchestrate its tools into a security report. But it had one big limitation: it only ran on my laptop.&lt;/p&gt;

&lt;p&gt;This time, I put it on the cloud for an external MCP client to reach it. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F243z0shfmmk3rat8aqip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F243z0shfmmk3rat8aqip.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That means introducing a new MCP transport mechanism, containerizing the server with Docker, deploying it on Hugging Face Spaces, and setting up Goose extensions to point at a public URL instead of running a local command. Let's dive in!s&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying out Foundational Concepts
&lt;/h2&gt;

&lt;p&gt;Let's start with what the &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25" rel="noopener noreferrer"&gt;MCP spec documentation&lt;/a&gt; says about transports and then look at where &lt;a href="https://goose-docs.ai/" rel="noopener noreferrer"&gt;Goose&lt;/a&gt;, the mcp host used in this example, fits in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streamable HTTP MCP Transport
&lt;/h3&gt;

&lt;p&gt;In MCP, a transport is the layer that defines how a client and a server exchange their JSON-RPC messages. The standard currently defines two transport mechanisms, and the journey in this chapter is about moving from one model to the other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;stdio&lt;/strong&gt;: The client launches the server as a subprocess and they communicate through standard input/output pipes. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streamable HTTP&lt;/strong&gt;: The MCP server runs as an independent HTTP service, typically exposed through a single endpoint. Clients send JSON-RPC messages using HTTP POST requests. The server can reply with a regular JSON response, or, when it needs to send multiple messages over time, it can return an SSE stream.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fueah5qy84i73w931017f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fueah5qy84i73w931017f.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So while in Chapters 1 and 2, &lt;code&gt;stdio&lt;/code&gt; was perfect for local development, for cloud deployment, I need a different model: one server running independently, reachable over the network, and updated in one place. That is what Streamable HTTP gives me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goose's Role as MPC host
&lt;/h2&gt;

&lt;p&gt;In Chapter 2, Goose was the host. It holds the conversation, runs the model, and decides when to call my MCP tools.&lt;/p&gt;

&lt;p&gt;What changes now is the transport. The tools, the resources, and the prompt my server exposes are identical to Chapter 2. Switching to Streamable HTTP doesn't touch any of them, it just changes &lt;em&gt;how the client reaches the server&lt;/em&gt;: instead of Goose launching my server as a subprocess, Goose connects to a server that's already running at a URL. And because the door is now a public URL, any other MCP-compatible host that supports Streamable HTTP could connect to it too &lt;/p&gt;

&lt;h3&gt;
  
  
  An Always-on MCP Server
&lt;/h3&gt;

&lt;p&gt;The server now has to stay up on its own, so it needs to live somewhere that isn't my laptop, which is why I containerize it and deploy it. &lt;/p&gt;

&lt;p&gt;One bit of cloud plumbing is worth naming now because it shows up in the practical section: many cloud platforms put a reverse proxy in front of your container. Long-lived open streams like SSE can work, but they are often fragile unless the platform is configured for them (proxies may enforce idle timeouts or drop connections). For simpler deployments like this one, I run the server in a stateless mode where each interaction is handled as a plain HTTP request/response&lt;/p&gt;

&lt;p&gt;With the standard in hand, the rest is implementation!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Teaching the Server to Speak Streamable HTTP
&lt;/h2&gt;

&lt;p&gt;The tempting move is to just switch the transport. Don't. Current Goose setup spawns the server over stdio, and flipping the transport would break it.&lt;/p&gt;

&lt;p&gt;A cleaner pattern is one codebase that can run either transport, chosen at runtime. The tools, resources, and the prompt do not change. What changes is the way the host reaches the server: locally through stdio, or remotely through Streamable HTTP.&lt;/p&gt;

&lt;p&gt;In practice, that means reading the transport from an environment variable and passing the right host and port when the server runs in HTTP mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;
&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github-stats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;stateless_http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json_response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In this section, you add the  tools, the resources, and the prompt, which stays exactly the same as chapter 2 shows
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP_TRANSPORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stdio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;stateless_http=True&lt;/code&gt; and &lt;code&gt;json_response=True&lt;/code&gt; is the reverse-proxy problem from the theory section&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, to test it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MCP_TRANSPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;streamable-http uv run server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is now live at &lt;code&gt;http://127.0.0.1:8000/mcp&lt;/code&gt;. I opened the &lt;a href="https://modelcontextprotocol.io/docs/tools/inspector" rel="noopener noreferrer"&gt;MCP Inspector&lt;/a&gt;, set the transport to &lt;strong&gt;Streamable HTTP&lt;/strong&gt;, pointed it at that URL, and saw the exact same tools, resource, and prompt I had over stdio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Containerizing it
&lt;/h2&gt;

&lt;p&gt;Hugging Face Spaces can host an arbitrary server as a Docker Space: you hand it a &lt;code&gt;Dockerfile&lt;/code&gt;, it builds a container and runs it on a public HTTPS URL. So the next step was packaging the server.&lt;/p&gt;

&lt;p&gt;Here's the shape of what the Space needs and what each piece is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github-stats-mcp/
├── server.py          # the MCP server (same as Chapter 2 + dual-transport add)
├── requirements.txt   # include mcp[cli], httpx, python-dotenv
├── Dockerfile         # set how HF builds and runs the container
├── README.md          # YAML header to tell HF this is a Docker Space
├── .dockerignore      # keeps junk and secrets out of the image
└── .env               # keep it local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dockerfile creates a non-root user (HF Spaces runs containers as a non-root user with uid 1000), and it bakes in the environment variables that flip the server into cloud mode: &lt;/p&gt;

&lt;p&gt;MCP_TRANSPORT=streamable-http, MCP_HOST=0.0.0.0 (so it's reachable from outside the container), and MCP_PORT=7860 (the port HF routes public traffic to)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 1000 user
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; user&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/home/user/.local/bin:$PATH"&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=user requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=user . .&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; MCP_TRANSPORT=streamable-http&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; MCP_HOST=0.0.0.0&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; MCP_PORT=7860&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 7860&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "server.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;.dockerignore&lt;/code&gt; keeps the noise (and, importantly, secrets, under &lt;code&gt;.env&lt;/code&gt;) out of the image&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Deploying to Hugging Face
&lt;/h2&gt;

&lt;p&gt;You can easily set the space SDK via &lt;a href="https://huggingface.co/docs/hub/en/spaces" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuho53wxxiaczgmvvq198.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuho53wxxiaczgmvvq198.png" alt=" " width="780" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The README needs a YAML header so HF treats it as a Docker Space. The two lines that actually matter are &lt;code&gt;sdk: docker&lt;/code&gt; and &lt;code&gt;app_port: 7860&lt;/code&gt; (which has to match &lt;code&gt;MCP_PORT&lt;/code&gt; in the Dockerfile).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GitHub&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Stats&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MCP"&lt;/span&gt;
&lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📊&lt;/span&gt;
&lt;span class="na"&gt;colorFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yellow&lt;/span&gt;
&lt;span class="na"&gt;colorTo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gray&lt;/span&gt;
&lt;span class="na"&gt;sdk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
&lt;span class="na"&gt;app_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7860&lt;/span&gt;
&lt;span class="na"&gt;pinned&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you create a new Docker Space, push &lt;code&gt;server.py&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;README.md&lt;/code&gt;, and &lt;code&gt;.dockerignore&lt;/code&gt; in your HF repo, and set the token as a secret in Settings. The server went live at a public &lt;code&gt;.hf.space&lt;/code&gt; URL&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Reconnecting Goose to the Cloud
&lt;/h2&gt;

&lt;p&gt;In Goose, I added the server as a remote extension pointing at the &lt;code&gt;.hf.space/mcp&lt;/code&gt; URL instead of a local command. Same model switching, same chat, now talking to a server in the cloud. And not just Goose: any MCP client anywhere can now connect to that URL.&lt;/p&gt;

&lt;p&gt;As last time,  I used &lt;a href="https://groq.com/" rel="noopener noreferrer"&gt;Qwen3-32b (via Groq)&lt;/a&gt; with my MCP tool activated and sent the prompt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Goose passed it to the LLM along with my tool descriptions&lt;/li&gt;
&lt;li&gt;The model decided it needed the tool&lt;/li&gt;
&lt;li&gt;Goose executed that call against my server&lt;/li&gt;
&lt;li&gt;The model formatted the result the way we wanted, grounded in the CHAOSS expert and context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used the same project as in Chapter 2:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dlmufkysgtypr2pfv0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dlmufkysgtypr2pfv0w.png" alt=" " width="800" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then it dropped the report:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8w47i84451rochez3gom.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8w47i84451rochez3gom.png" alt=" " width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication Note
&lt;/h2&gt;

&lt;p&gt;At the end of Chapter 2, I said I would add a security layer with OpenID Connect in this cloud chapter. Once I actually deployed the server, I changed my mind. There are a few reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The deployment itself deserved a full chapter: Adding auth on top would have buried the main story (moving the MCP server from stdio to a public cloud environment). That's why authentication will get its own treatment in a later chapter&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deploying it publicly made the auth problem more concrete: A public server changes the threat model immediately: anyone with the URL can call its tools, and every call consumes the GitHub token’s rate limit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I am treating the deployment as a temporary sandbox, not a production setup: The server exposes no sensitive data, and the GitHub token is deliberately constrained: fine-grained, read-only, public-repositories-only, and with no account-level permissions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But please note that this does not make authentication unnecessary. It only makes the remaining risk acceptable for this specific experiment (e.g., someone could burn through the token’s read rate limit for a while, but they could not access private data or perform write actions as me). For any persistent, shared, write-capable, or private-data-connected server, I would add authentication before exposing it publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Remarks and Questions for the Next Chapter
&lt;/h2&gt;

&lt;p&gt;This chapter shows the moment my server stopped being a local process someone runs and became a hosted service available at a URL. The server is deployed, and anyone can call it. &lt;/p&gt;

&lt;p&gt;Deploying made the next question worth testing: &lt;strong&gt;What does MCP actually improve in the quality of the response, compared with asking the same model without MCP tools or with plain web search?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the next chapter, I build a second server based on the other CHAOSS practitioner guide and metrics set: Responsiveness⁠, and its open knowledge. Then I run an experiment inspired by a community peer at CHAOSS, asking the same question to the same model in three different ways: with my MCP tools, without my MCP tools, and with plain web search.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>docker</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a GitHub Stats MCP Server with Security Metrics</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Sun, 31 May 2026 10:15:41 +0000</pubDate>
      <link>https://dev.to/anajsana95/building-a-github-stats-mcp-server-with-security-metrics-3omi</link>
      <guid>https://dev.to/anajsana95/building-a-github-stats-mcp-server-with-security-metrics-3omi</guid>
      <description>&lt;p&gt;&lt;em&gt;👋 This is the second chapter of a series where I document what I'm learning about Model Context Protocol Architecture and Tool implementations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/anajsana95/the-weekend-i-fell-down-the-mcp-rabbit-hole-a"&gt;Chapter 1&lt;/a&gt;, I built a simple Calculator MCP Server. This time, I connected my MCP server to an external API, added the two other MCP structures (Resources and Prompts), and ended up with something useful for teams evaluating open source dependencies and ecosystem health: a security risk assessment tool powered by&lt;a href="https://chaoss.community/" rel="noopener noreferrer"&gt;CHAOSS&lt;/a&gt; metrics, helping practitioners better interpret project health&lt;/p&gt;

&lt;p&gt;Let's first get into the theory and new concepts&lt;/p&gt;

&lt;h2&gt;
  
  
  An Introduction to MCP Resources and Prompts
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o6e807v40ms8e6vxkxw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o6e807v40ms8e6vxkxw.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We briefly mentioned in the last post about the MCP's primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Tools: Grants agency to the AI and are functions the LLM executes (e.g., get_repo_info())&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resources: Provides safe, contextual data the LLM can read a URL, a file, or an API response&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prompts: Structure the conversation with expert context templates (e.g., &lt;em&gt;You are a data scientist analyzing CHAOSS metrics&lt;/em&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this project, I needed all three: Tools to fetch GitHub data, Resources to load the CHAOSS guide, and Prompts to give the LLM the right expert context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CHAOSS and why does it matter to build GitHUb Stats?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chaoss.community/" rel="noopener noreferrer"&gt;CHAOSS&lt;/a&gt; is a Linux Foundation project that develops metrics and frameworks for measuring the health of open source communities.&lt;/p&gt;

&lt;p&gt;Their &lt;a href="https://chaoss.community/about-chaoss-practitioner-guides/" rel="noopener noreferrer"&gt;Practitioner Guides&lt;/a&gt; are particularly useful because they take complex community health topics, such as security, contributor sustainability, and responsiveness, and translate them into actionable metrics. These metrics can help anyone interpreting open source project data develop insights to improve the project’s health.&lt;/p&gt;

&lt;p&gt;The one I focused on building my server is the CHAOSS Security Practitioner Guide, which centers on three primary metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://openssf.org/projects/best-practices-badge/" rel="noopener noreferrer"&gt;OpenSSF Best Practices Badge&lt;/a&gt;: whether the project follows OpenSSF security best practices.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/chaoss/wg-risk/blob/main/focus-areas/dependency-risk-assessment/libyear.md" rel="noopener noreferrer"&gt;Libyears&lt;/a&gt;: how outdated the project's dependencies are. &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Libyears concept: if your project uses a dependency that's 2 years behind its latest release, that's 2 libyears of lag. Studies show that projects with high libyears are 4x more likely to have security vulnerabilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Release Frequency: how often security fixes and updates land in a release. A project that releases rarely may have fixes in the code that users can't access yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These three metrics together give a starting point for assessing the security posture of any open source project, which is something that an Open Source Manager in the OSPO could need when evaluating dependencies or reporting to security teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Setting up the project
&lt;/h2&gt;

&lt;p&gt;Used the same stack as Chapter 1 (Python, uv, FastMCP) with two extra dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;httpx&lt;/code&gt;: modern Python HTTP client for calling the GitHub API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python-dotenv&lt;/code&gt;: for managing the GitHub token securely via a .env file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The MCP Tools
&lt;/h2&gt;

&lt;p&gt;The key difference from Chapter 1 is that your MCP server now talks to an external API. I added five tools aligned wiht the CHAOSS practitioner guide to security:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github-stats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_repo_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get basic info and stats for a GitHub repository&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_release_frequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get release frequency info&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_security_advisories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get known security advisories for a GitHub repository&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_dependency_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check what dependency files exist in a repo&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_libyears&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Calculate CHAOSS Libyears metric&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last tool called Libyears was the most interesting to add. It uses GitHub's Dependency Graph SBOM API to get all dependencies, then queries PyPI for each package to find how far behind the current version is from the latest. A high libyears score means outdated dependencies, which, according to CHAOSS, correlates with higher security risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Adding Resources and Prompts
&lt;/h2&gt;

&lt;p&gt;This is where MCP gets powerful beyond just tool calling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource: the CHAOSS Security Guide
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@mcp.resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaoss://security-guide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_chaoss_security_guide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;CHAOSS Security Practitioner Guide content&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://chaoss.community/practitioner-guide-security/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;[^&amp;gt;]+&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fetches the CHAOSS Security Practitioner Guide and makes it available to the LLM as a readable document. The host can load it into context when needed (so the model doesn't just have your tool results, it has the actual framework for interpreting them).&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt: the CHAOSS Security Analyst
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@mcp.prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chaoss_security_analyst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Expert prompt for CHAOSS security risk assessment from an OSPO perspective&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are a data scientist specializing in open source security metrics for OSPOs.

Your role is to analyze GitHub repositories using CHAOSS Security Practitioner Guide 
metrics and produce clear security risk assessments that OSPOs can present to their 
security teams.

When analyzing a repository, always:

1. INTERPRET metrics using these CHAOSS thresholds:
   Libyears: 0-2 = low risk, 2-5 = moderate risk, 5+ = high risk
   Release Frequency: weekly/monthly = healthy, quarterly or less = concern
   Security Advisories: 0 = good, any critical = immediate action needed

2. CONTEXTUALIZE results considering project size, team, and ecosystem.

3. STRUCTURE your output as a risk assessment report:
   Executive Summary, Metrics Summary, Risk Level, Recommendations, Why this matters
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Prompt gives the LLM a specific expert role: &lt;em&gt;you are a data scientist building a security report for an OSPO to present to their security team&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Testing in the MCP Inspector
&lt;/h2&gt;

&lt;p&gt;Same workflow as Chapter 1, run your server.py, open the Inspector, verify all 5 tools, 1 resource, and 1 prompt are visible and working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91pymiok0c5skkh516pe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91pymiok0c5skkh516pe.png" alt=" " width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Connect to Goose
&lt;/h2&gt;

&lt;p&gt;I decided to test on a large, well-known open source ML framework.&lt;/p&gt;

&lt;p&gt;I then connected the server to Goose exactly as in Chapter 1, then asked: &lt;em&gt;Analyze [insert open source project name] security using all available GitHub-stats tools and give me a risk assessment with recommendations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After some model experimentation (Llama 3.3-70b had trouble with multiple tools, Qwen3-32b worked well), Goose called all 4 tools, synthesized the results with the CHAOSS context, and produced this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Security Risk Assessment — [open source project]

Security Advisories: 5 total (including 1 critical — arbitrary code execution when loading malicious [open source project] checkpoint with weights_only=True, published January 2026)
Release Frequency: 30 releases, latest v2.12.0 — active and healthy
Open Issues: 18,496 — high volume, warrants triage
Risk Level: HIGH

Recommendations:

🚨 Patch unresolved security advisories immediately
⚠️ Review high number of open issues — 32% untriaged
✅ Release cadence is healthy — keep it up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few considerations when reading this test report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A "HIGH" risk level here does not mean this is a bad or insecure project. For a project of the scale I was analyzing (hundreds of thousands of users, a dedicated security team, etc), these numbers need to be interpreted in context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The high open issue count reflects high community activity, not neglect. The disclosed security advisories are being actively managed and publicly reported, which is actually a sign of good security hygiene (projects that disclose vulnerabilities are more trustworthy than those that don't)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CHAOSS metrics are a starting point for conversation, not a final verdict. Security risk always depends on domain context (the same metric means something very different in a healthcare system versus a research prototype). &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Final security decisions should always involve human judgment, domain expertise, and a deeper understanding of how the project is used in your specific context&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bonus point: Showing this in a web app
&lt;/h2&gt;

&lt;p&gt;The transport method between Goose and our server is &lt;code&gt;stdio&lt;/code&gt;. This means they communicate via &lt;code&gt;stdin/stdout&lt;/code&gt; pipes. This is perfect for local development, but it means your server only exists while our machine is running and Goose is open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fst9512p9np9hgip1fbbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fst9512p9np9hgip1fbbo.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, what if I want to share this as an open web dashboard and not just in a local chat interface?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Streamable HTTP&lt;/code&gt; is the MCP way of transport that lets an MCP server be exposed over HTTP, rather than only as a local process over &lt;code&gt;stdio&lt;/code&gt;. In this scenario, client-to-server communicates using HTTP POST requests. When the server needs to send multiple messages over time, it responds with an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" rel="noopener noreferrer"&gt;SSE stream&lt;/a&gt; for server-to-client messages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0in4wa54s00suo4picb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0in4wa54s00suo4picb.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a few use cases that require that transport:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MCP Remote: server is deployed to the cloud, accessible via a public URL (e.g, This is already how the connectors in claude.ai work when you connect to Asana, Google Drive, etc)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MCP Apps: browser UIs that connect directly to your MCP server without needing a desktop AI host at all. A new experimental feature in the &lt;a href="https://modelcontextprotocol.io/docs/tools/inspector" rel="noopener noreferrer"&gt;MCP Inspector&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're curious about where MCP official transport layers need to evolve for edge environments (like manufacturing, healthcare, or automotive) where latency and connectivity can't be taken for granted, this talk explores &lt;a href="https://aaif.io/blog/mcp-was-designed-for-the-cloud-the-edge-has-different-rules/" rel="noopener noreferrer"&gt;Binary MCP with Protocol Buffers for constrained networks&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  What's coming in Chapter 3
&lt;/h1&gt;

&lt;p&gt;We will be exploring how to build a cloud-deployed version of our MCP server while adding a security layer using &lt;a href="https://openid.net/developers/how-connect-works/" rel="noopener noreferrer"&gt;OpenID Connect&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>mcp</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The weekend I fell down the MCP rabbit hole</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Mon, 18 May 2026 15:34:33 +0000</pubDate>
      <link>https://dev.to/anajsana95/the-weekend-i-fell-down-the-mcp-rabbit-hole-a</link>
      <guid>https://dev.to/anajsana95/the-weekend-i-fell-down-the-mcp-rabbit-hole-a</guid>
      <description>&lt;p&gt;&lt;em&gt;👋This is the start of a series where I document what I'm learning about Model Context Protocol Architecture and Tool implementations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'd been reading about MCP for a while and felt like I had a good grasp of the concepts, but I hadn't actually shared any builds yet. So this series is me finally putting my hands on it, documenting what I'm learning (from Zero to Beginner), and sharing it in case it's useful to others going through the same thing&lt;/p&gt;

&lt;p&gt;My learning is coming from a mix of things: Some books, a few YouTube tutorials, conversations with DevRel peers exploring similar things, and a fair amount of AI assistance to help me understand concepts along the way 😄 Let's start!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1m8rifhv0lzy44zfribg.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1m8rifhv0lzy44zfribg.PNG" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  An Intro to Model Context Protocol (MCP)
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://www.amazon.com/MCP-Standard-Developers-Building-Universal/dp/B0G2WVSXC6" rel="noopener noreferrer"&gt;the MCP Standard book, by Srinivasan Sekar&lt;/a&gt; there is a perfect analogy to explain the problem MCP solves. &lt;/p&gt;

&lt;p&gt;Picture this: you have "M" AI models and "N" tools you want to connect to them. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbyk6vs1pjdx2h3uao2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbyk6vs1pjdx2h3uao2f.png" alt=" " width="799" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Old Way: Without a standard, you'd have to build a unique integration for every single combination. That’s ‭$M \times N$‬‭‬ different connections and a total nightmare to maintain!&lt;/li&gt;
&lt;li&gt;The MCP Way: MCP acts as the universal plug. You build your tool once to the MCP standard, and it instantly works with any model that speaks it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this scenario sounds familiar, it's what happened with containerization before &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt;. Kubernetes came along and said: &lt;em&gt;Here's the standard&lt;/em&gt;. MCP is doing the same thing for AI tooling.&lt;/p&gt;

&lt;h1&gt;
  
  
  The 3 MCP Roles to Understand
&lt;/h1&gt;

&lt;p&gt;Before building anything, there are three roles to wrap your head around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Host&lt;/strong&gt; is the app the user interacts with directly. It decides which tools to call, keeps the conversation context, and manages clients. Think Claude Desktop, Goose, VS Code, Cursor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Client&lt;/strong&gt; lives inside the host. It maintains the connection with a concrete MCP Server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Server&lt;/strong&gt; answers client requests and exposes tools, resources, and prompts through the MCP protocol. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jwklb040dvz61nkkbl3.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jwklb040dvz61nkkbl3.PNG" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before MCP, the AI agent was doing everything (fetching data, formatting it, calling the model, returning the answer. One giant piece of logic. Hard to test and hard to scale). After MCP, the agent only does one thing: reason.&lt;/p&gt;

&lt;p&gt;Now that we’ve covered the high-level concepts of MCP, let's dive into the practical configuration!&lt;/p&gt;

&lt;h1&gt;
  
  
  Level 0: Let's build a Calculator
&lt;/h1&gt;

&lt;p&gt;Not glamorous, but the perfect &lt;em&gt;hello world&lt;/em&gt; for understanding the fundamentals&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Using the MCP Python SDK
&lt;/h2&gt;

&lt;p&gt;To build an MCP server, you don't need to implement the protocol from scratch. There is an official open source SDK (under MIT license) under the &lt;code&gt;modelcontextprotocol&lt;/code&gt; organisation on GitHub.&lt;/p&gt;

&lt;p&gt;Without the SDK you'd have to manually handle the JSON-RPC messages that flow between host and server via stdin/stdout, implement the handshake, route requests, serialize responses, etc. It's totally doable, but the SDK abstracts all of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Create the Server (server.py)
&lt;/h2&gt;

&lt;p&gt;Inside the Python SDK, I decided to go with FastMCP, a specific class within the SDK that hides the complex protocol logic.&lt;/p&gt;

&lt;p&gt;Here is the basic calculator, under a simple server.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Multiply 2 numbers&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="c1"&gt;# + add, subtract, divide...
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡In this scenario, server.py is the MCP Server, which exposes tools through the protocol.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Test Server and Tools
&lt;/h2&gt;

&lt;p&gt;Before connecting to any AI host, I tested my server with the official MCP Inspector. Note I'm using &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; to manage the environment and run the development tools when running the inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run mcp dev server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A browser UI opens. You'll see: status "Connected", server name "calculator", SDK version, and the history.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The MCP Inspector is acting as both Host and Client here: doing the handshake, maintaining the connection, calling your tools&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Connect to a Host
&lt;/h2&gt;

&lt;p&gt;I tested three options:&lt;/p&gt;

&lt;h3&gt;
  
  
  🪿 Goose
&lt;/h3&gt;

&lt;p&gt;Goose is an &lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;open-source AI agent&lt;/a&gt; that can act as an MCP Client and has a visual form to configure the extension name, type (&lt;code&gt;STDIO&lt;/code&gt;), and the command it runs every time it starts. Internally, it stores config in a YAML file you never touch. &lt;/p&gt;

&lt;h3&gt;
  
  
  ✳️ Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Claude Desktop needs you to configure a manual JSON such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"calculator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/uv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--directory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server.py"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🤖 VS Code + Copilot
&lt;/h3&gt;

&lt;p&gt;Same JSON approach as Claude Desktop inside settings.json.&lt;/p&gt;

&lt;p&gt;Here is a quick comparison of the three environments I’ve been testing, including a breakdown of their models, costs, and setup methods:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Goose&lt;/th&gt;
&lt;th&gt;Claude Desktop&lt;/th&gt;
&lt;th&gt;VS Code + Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Llama 3.3-70b&lt;/td&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP Config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Visual UI - YAML config&lt;/td&gt;
&lt;td&gt;Manual JSON config&lt;/td&gt;
&lt;td&gt;Manual JSON config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14,400 req/day&lt;/td&gt;
&lt;td&gt;Pay per use&lt;/td&gt;
&lt;td&gt;Free tier (50 chats/month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local model option&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (Ollama)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Each host manages its own MCP Client internally. You don't write client code (the host handles that). Your job as a "server developer" is just to make sure your server speaks valid MCP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Full MCP Cycle Overview
&lt;/h2&gt;

&lt;p&gt;As a takeaway, let's summarize the process into this 8-step cycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 User type the query and the Host receives it&lt;/li&gt;
&lt;li&gt;2 Host passes it to the LLM for initial reasoning. LLM decides: "I need an external tool for this"&lt;/li&gt;
&lt;li&gt;3 LLM signals tool invocation intent to the Host&lt;/li&gt;
&lt;li&gt;4 Host delegates to the Client (the messenger)&lt;/li&gt;
&lt;li&gt;5 Client serializes the request in JSON-RPC and sends it to your MCP Server via stdin/stdout&lt;/li&gt;
&lt;li&gt;6 Server validates and executes your Python function&lt;/li&gt;
&lt;li&gt;7 Server returns a standard MCP response&lt;/li&gt;
&lt;li&gt;8 Host shows the result to the user&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  What's Next?
&lt;/h1&gt;

&lt;p&gt;The calculator was self-contained (all logic lived in server.py). In my next post, I'll share my experience in &lt;a href="https://dev.to/anajsana95/building-a-github-stats-mcp-server-with-security-metrics-3omi"&gt;building a GitHub Stats server&lt;/a&gt; to talk to an external API and how to handle authentication, rate limits, and what data to expose to the LLM.&lt;/p&gt;

&lt;h1&gt;
  
  
  Resources
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.com/MCP-Standard-Developers-Building-Universal/dp/B0G2WVSXC6" rel="noopener noreferrer"&gt;A Developer's Guide to Building Universal AI Tools Using MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//github.com/modelcontextprotocol/python-sdk"&gt;Python SDK Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//github.com/modelcontextprotocol/inspector"&gt;MCP Inspector Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//github.com/block/goose"&gt;Goose Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;Goose Repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//console.groq.com"&gt;Groq Console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//ollama.ai"&gt;Ollama Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Dockerizing your first web app with python and flask</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Sun, 19 Jul 2020 21:02:28 +0000</pubDate>
      <link>https://dev.to/anajsana95/dockerizing-your-first-web-app-with-python-and-flask-2fn2</link>
      <guid>https://dev.to/anajsana95/dockerizing-your-first-web-app-with-python-and-flask-2fn2</guid>
      <description>&lt;p&gt;Second part of &lt;a href="https://dev.to/anajsana95/how-to-measure-covid-19-impact-on-jitsi-project-with-python-and-elasticsearch-154j"&gt;how to measure covid-19 impact on Jitsi project with python and elasticsearch&lt;/a&gt;. This time, my colleagues (&lt;a href="https://www.linkedin.com/in/david-dur%C3%A1n-prieto-86326b17a/" rel="noopener noreferrer"&gt;David&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/gerardoadrianaguirrevivar/" rel="noopener noreferrer"&gt;Gerardo&lt;/a&gt;) and I (&lt;a href="https://www.linkedin.com/in/ana-jim%C3%A9nez-santamar%C3%ADa/" rel="noopener noreferrer"&gt;Ana&lt;/a&gt;) built a web app with Python technology and Flask framework and dockerized it. Here we explain you how&lt;/p&gt;

&lt;h1&gt;
  
  
  Docker architecture
&lt;/h1&gt;

&lt;p&gt;First, we have to build a docker application with three containers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ElasticSearch image&lt;/li&gt;
&lt;li&gt;Kibana image&lt;/li&gt;
&lt;li&gt;Web app image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F0fc1l36wew4xqcmll5ak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F0fc1l36wew4xqcmll5ak.png" alt="Alt Text" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For ElasticSearch and Kibana, we use pre-built images from &lt;strong&gt;DockerHub&lt;/strong&gt; using 7.8.0 version. Web application uses a redis-alpine image.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: The reason why using alpine, is because this alternative is smaller and more resource efficient than traditional GNU/Linux distributions (such as Ubuntu)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Required files and configuration
&lt;/h1&gt;

&lt;p&gt;You can get the required materials on &lt;a href="https://gitlab.com/msdatascienceurjc/docker-jitsi-app" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. The image below shows the different files you will get by cloning the repo&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmj5uq81ywtnzv85supi8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmj5uq81ywtnzv85supi8.png" alt="Alt Text" width="748" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker-compose.yml structure
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; file shows the structure explained in previous section&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5000:5000&lt;/span&gt;
  &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;elastic&lt;/span&gt;
&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis:alpine"&lt;/span&gt;
&lt;span class="na"&gt;es01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.elastic.co/elasticsearch/elasticsearch:7.8.0&lt;/span&gt;
   &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;es01&lt;/span&gt;
   &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.name=es01&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cluster.name=es-docker-cluster&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;discovery.type=single-node&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bootstrap.memory_lock=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ES_JAVA_OPTS=-Xms1024m&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-Xmx1024m"&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="na"&gt;ulimits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;soft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
        &lt;span class="na"&gt;hard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;data01:/usr/share/elasticsearch/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;9200:9200&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;elastic&lt;/span&gt;
  &lt;span class="na"&gt;kib01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.elastic.co/kibana/kibana:7.8.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kib01&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5601:5601&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ELASTICSEARCH_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://es01:9200&lt;/span&gt;
      &lt;span class="na"&gt;ELASTICSEARCH_HOSTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://es01:9200&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;elastic&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;data01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;data02&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;data03&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;elastic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a look at the main tags stated at &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ports: configures docker port with host machine port where docker runs. In this case, 5000, 5601 and 9200.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Image: docker image that is downloaded for the required service&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Networks: this is named as 'elastic' in order to connect the three services&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment: configures environment variables essential to operate among services, such as RAM memory parameters. Within Kibana service, it is necessary to define the variables &lt;code&gt;ELASTICSEARCH_URL&lt;/code&gt; and &lt;code&gt;ELASTICSEARCH_HOSTS&lt;/code&gt;, in order to link it with the Elasticsearch service.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dockerfile configuration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; has the required steps to configure the web application to link with our python script that extract and store the data in an Elasticsearch cluster, that at the same time imports &lt;code&gt;impact_dashboard.ndjson&lt;/code&gt; on Kibana for previous visualization.&lt;/p&gt;

&lt;p&gt;It runs on alpine distribution to copy the required folders to get the app running. Moreover, and thanks to &lt;code&gt;requirements.txt&lt;/code&gt;, you can add all the dependencies that the python script needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.7-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /code&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FLASK_APP jitsi-data.py&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FLASK_RUN_HOST 0.0.0.0&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; git
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; tk-dev
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; gcc musl-dev linux-headers
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; mapping.json mapping.json&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; templates templates&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; impact_dashboard.ndjson impact_dashboard.ndjson&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["flask", "run"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  How to start containters
&lt;/h1&gt;

&lt;p&gt;In order to start the application (with docker and docker-compose previously installed) open your terminal and execute the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will initiate the different commands stated in dockerfile and download the required docker images. Once that's finished, you can check that Elasticsearch (localhost:9200) and Kibana (localhost:5601) are succesfully running.&lt;/p&gt;

&lt;p&gt;Now, it's time to go where python Flask application web app is located, using port &lt;code&gt;5000&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbdgpwjr7erskgbabf5jm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbdgpwjr7erskgbabf5jm.png" alt="Alt Text" width="722" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking on &lt;code&gt;start button&lt;/code&gt; will initialize data extraction from Jitsi git data (check &lt;code&gt;jitsi-data.py&lt;/code&gt; for more details) and will store such data to our ElasticSearch. Finally, it will import &lt;code&gt;impact_dashboard.ndjson&lt;/code&gt; to our Kibana, allowing us to interactively play with the data.&lt;/p&gt;

&lt;p&gt;Once the process is finished, the browser will show the next message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1sp3n4dv335v5fa8erp9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1sp3n4dv335v5fa8erp9.png" alt="Alt Text" width="717" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, we can see if our Elasticsearch index and Kibana dashboard have successfully being added to our instances:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkjmu8sf90cjcxyzt3eoy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkjmu8sf90cjcxyzt3eoy.png" alt="Alt Text" width="722" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fuhymzlhkkknaf0m9k629.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fuhymzlhkkknaf0m9k629.png" alt="Alt Text" width="612" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By default the time filter is set to the last 15 minutes. You can use the Time Picker to change the time filter or select a specific time interval or time range in the histogram at the top of the page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fatm4deo5nw6u92s6fwg4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fatm4deo5nw6u92s6fwg4.png" alt="Alt Text" width="722" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;et voilà&lt;/em&gt; now you have a cool dashboard up and running to analyze how a pandemic can impact Jitsi software development activity.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bonus point: uploading a Docker image to Docker Hub
&lt;/h1&gt;

&lt;p&gt;With a Docker Hub account, you can build an image where &lt;code&gt;dockerfile&lt;/code&gt; is located. Simply type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker build -t dockerhubID/DockerHubreponame:imagetag .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, upload the image to your Docker Hub repo (you can create the repo using the Docker Hub UI&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo docker push dockerhubID/DockerHubreponame:imagetag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the image is uploaded to Docker Hub, any user can use and run the app with the following docker-compose.yml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daviddp92/jitsi-data-extraction:1.0.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5000:5000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;elastic&lt;/span&gt;
  &lt;span class="na"&gt;es01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.elastic.co/elasticsearch/elasticsearch:7.8.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;es01&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.name=es01&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cluster.name=es-docker-cluster&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;discovery.type=single-node&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bootstrap.memory_lock=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ES_JAVA_OPTS=-Xms1024m&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-Xmx1024m"&lt;/span&gt;
    &lt;span class="na"&gt;ulimits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;memlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;soft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
        &lt;span class="na"&gt;hard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;data01:/usr/share/elasticsearch/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;9200:9200&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;elastic&lt;/span&gt;
  &lt;span class="na"&gt;kib01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.elastic.co/kibana/kibana:7.8.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kib01&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5601:5601&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ELASTICSEARCH_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://es01:9200&lt;/span&gt;
      &lt;span class="na"&gt;ELASTICSEARCH_HOSTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://es01:9200&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;elastic&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;data01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;data02&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;data03&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;elastic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;We have learned how to dockerize a web app with Python technology and Flask framework using docker-compose. We also saw how to use and run the application using Docker Hub images.&lt;/p&gt;

&lt;p&gt;What do you think about this project? What should we improve? Feedback is more than welcome :)&lt;/p&gt;

</description>
      <category>docker</category>
      <category>python</category>
    </item>
    <item>
      <title>How to measure Covid-19 impact on Jitsi project with Python and ElasticSearch</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Fri, 03 Jul 2020 13:13:32 +0000</pubDate>
      <link>https://dev.to/anajsana95/how-to-measure-covid-19-impact-on-jitsi-project-with-python-and-elasticsearch-154j</link>
      <guid>https://dev.to/anajsana95/how-to-measure-covid-19-impact-on-jitsi-project-with-python-and-elasticsearch-154j</guid>
      <description>&lt;p&gt;The situation with Covid-19 limits the possibility of face-to-face communication. Remote work is now a reality, and the demand for video conferencing such as &lt;a href="https://jitsi.org/jitsi-meet/" rel="noopener noreferrer"&gt;Jitsi Meet&lt;/a&gt; continues breaking records.&lt;/p&gt;

&lt;p&gt;Could it be possible that &lt;a href="https://github.com/jitsi/jitsi-meet" rel="noopener noreferrer"&gt;Jitsi’s software development&lt;/a&gt; activity and community are on high-demand too?&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1235916871783919617-496" src="https://platform.twitter.com/embed/Tweet.html?id=1235916871783919617"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1235916871783919617-496');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1235916871783919617&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;This post summarizes one of the analysis that my colleagues &lt;a href="https://www.linkedin.com/in/david-dur%C3%A1n-prieto/" rel="noopener noreferrer"&gt;David Duran&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/gerardoadrianaguirrevivar/" rel="noopener noreferrer"&gt;Gerardo Aguirre&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/ana-jim%C3%A9nez-santamar%C3%ADa/" rel="noopener noreferrer"&gt;I&lt;/a&gt; did for our M.S. in Data Science. Following the &lt;a href="https://en.wikipedia.org/wiki/GQM" rel="noopener noreferrer"&gt;GQM approach&lt;/a&gt;, we defined a clear goal and some related questions. Then, we decided the best metrics to answer those questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;Analyze the impact that a global pandemic such as Covid-19 has on Jitsi&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;p&gt;Did Covid-19 countermeasures change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jitsi’s activity patterns?&lt;/li&gt;
&lt;li&gt;Jitsi’s diversity patterns? &lt;/li&gt;
&lt;li&gt;Jitsi’s community risk? &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we are trying to measure impact, we need to see potential changes. For instance, did software development activity change during covid-19 outbreak compared to last year?&lt;/p&gt;

&lt;p&gt;We selected Feb 1st- May 31st 2019 and 2020 as our timeframe to measure such impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contributor Activity&lt;/th&gt;
&lt;th&gt;Contributor Diversity&lt;/th&gt;
&lt;th&gt;Community Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Contributors&lt;/td&gt;
&lt;td&gt;Organization Diversity&lt;/td&gt;
&lt;td&gt;Bus Factor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total Commits&lt;/td&gt;
&lt;td&gt;Gender diversity&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Timezone Diversity&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Total Contributors: number of authors that made a commit over a period of time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Total Commits: number of commits over a period of time&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsqv6d5djj15gmod6pngq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsqv6d5djj15gmod6pngq.png" alt="Alt Text" width="742" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organization Diversity: different email domains over a period of time. Here we can find company’s domains (e.g jitsi, 8x8, etc) or individual’s (gmail, users, etc)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fblv3ynp7xys9z8ol8buz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fblv3ynp7xys9z8ol8buz.png" alt="Alt Text" width="738" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gender Diversity: total share of female and male author names that made a commit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F12ak5i88ifxtmtplk6mp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F12ak5i88ifxtmtplk6mp.png" alt="Alt Text" width="306" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timezone diversity: existing authors on each timezone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fg8zpkasr9r9yljwuaybv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fg8zpkasr9r9yljwuaybv.png" alt="Alt Text" width="500" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bus Factor: minimum number of contributors needed to make the 50% of git code on Jitsi over a period of time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools used
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffia7rw50qwwcd3ui2u2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffia7rw50qwwcd3ui2u2b.png" alt="Alt Text" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data gathering and storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For data gathering we used an open source tool called Perceval. &lt;a href="https://github.com/chaoss/grimoirelab-perceval" rel="noopener noreferrer"&gt;Perceval&lt;/a&gt; offers a Python module for retrieving data from repositories related to software development. It works with many data sources, from Git repositories and GitHub projects to mailing lists, Gerrit or StackOverflow.&lt;/p&gt;

&lt;p&gt;For this specific analysis, we will just need Jitsi Git data.&lt;/p&gt;

&lt;p&gt;If you are running Jitsi-data.py available in &lt;a href="https://gitlab.com/msdatascienceurjc/ELK-perceval/-/blob/master/jitsi-data.py" rel="noopener noreferrer"&gt;this Gitlab repo&lt;/a&gt;, make sure you have installed perceval first&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pip install perceval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can simply use perceval git module on python with perceval.backends.core.git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;elasticsearch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Elasticsearch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;  &lt;span class="c1"&gt;# to specify a specific timeframe
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gender_guesser.detector&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;gender&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;perceval.backends.core.git&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Git&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dateutil.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perceval has 2 arguments for Git: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URI: the repository or list of repos from GitLab or GitHub you want to analyze&lt;/li&gt;
&lt;li&gt;Gitpath: usually something like /tmp/repo-name.git
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# It has 2 arguments, uri and gitpath: repo = Git(uri=repository, gitpath='/tmp/'+repository.git)
&lt;/span&gt;&lt;span class="n"&gt;data_repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://github.com/jitsi/jitsi-meet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gitpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tmp/jitsi-meet.git&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moreover, we use &lt;a href="https://elasticsearch-py.readthedocs.io/en/master/" rel="noopener noreferrer"&gt;Python Elasticsearch client&lt;/a&gt; to store data and mapping from python script to our ElasticSearch instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Instance of elasticsearch to put the data into ElasticSearch/Kibana system (it is necessary to have both up on localhost)
&lt;/span&gt;    &lt;span class="n"&gt;elastic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Elasticsearch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Read mapping json file to ElasticSearch
&lt;/span&gt;    &lt;span class="n"&gt;mapping_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mapping.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mapping_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Creation of ElasticSearch index using the mapping
&lt;/span&gt;    &lt;span class="n"&gt;index_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo_jitsi_index_def&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elastic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;  &lt;span class="c1"&gt;# ignore 400 already exists code
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Data transformation and mapping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Perceval gives us raw git data with a lot of information over a period of time (we specify this thanks to datetime python package): Commit, Author, Lines added, Lines removed, Email, Commit Date and more.&lt;/p&gt;

&lt;p&gt;However, in order to analyze contributor diversity, we need to create new fields with the data provided. That's why we add organization (based on email domains), gender (using a &lt;a href="https://pypi.org/project/gender-guesser/" rel="noopener noreferrer"&gt;gender detector&lt;/a&gt; python package) and timezone fields (cleaning item field)&lt;/p&gt;

&lt;p&gt;You can see how we iterate through the different fields provided by Perceval to get the data in our &lt;a href="https://gitlab.com/msdatascienceurjc/ELK-perceval/-/blob/master/jitsi-data.py" rel="noopener noreferrer"&gt;GitLab repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we generate these new fields, we create our Elasticsearch mapping, transforming item data into a new structure matching the mapping for ElasticSearch&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data visualization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are running unity-data.py script with your Elasticsearch and Kibana instance opened, you can go to your Kibana localhost (usually &lt;code&gt;5601&lt;/code&gt;) &amp;gt; management &amp;gt; index patterns and click on create index pattern. You should see the index name given at jitsi-data.py (by default is &lt;code&gt;repo_jitsi_index_def&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc65qt40ifcq766i971dg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc65qt40ifcq766i971dg.gif" alt="Alt Text" width="720" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We then elaborate a Kibana dashboard with the specific visualizations that fits best with our defined metrics. We called it “impact_dashoard”&lt;/p&gt;

&lt;p&gt;At a first glance, we can see an uncommon behavior on Jitsi’s activity since February 2020:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fdi5lvgrp62ildldelwhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fdi5lvgrp62ildldelwhi.png" alt="Alt Text" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And when comparing 2019 period with 2020, results are also quite interesting:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fcnxl43zhjvjr95fki4yg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fcnxl43zhjvjr95fki4yg.gif" alt="Alt Text" width="600" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Image below highlights major changes regarding contributor’s diversity and activity:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fch8yeo8d20moarqgbinu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fch8yeo8d20moarqgbinu.png" alt="Alt Text" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contributor’s Activity increased:&lt;/em&gt; there is an increase in commit activity, lines added and removed, authors and organizations involved on Jitsi’s software development&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contributor's Diversity increased:&lt;/em&gt; It’s interesting to see that women activity was under 1% last year and since covid-19 started, it increased to 8%. Still a low number, but is a good indicator. &lt;/p&gt;

&lt;p&gt;Also, we see some author activity in Asia that didn’t existed before.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: there are “unknown” data that our gender detector couldn’t guess. The main reason is because some Jitsi’s developers have non-occidental characters and our gender detector is optimized for occidental names. We did an exhaustive cleaning for the most relevant names though, reducing our “unknown” data, but we couldn’t erase it completely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What about Community Risk?
&lt;/h2&gt;

&lt;p&gt;To answer our last question and metric, we work on a query using &lt;a href="https://jupyter.org/" rel="noopener noreferrer"&gt;Jupyter Notebook&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The Bus Factor for developers isn’t something new: It is the minimum number of team members that have to suddenly disappear from a project before the project stalls due to lack of knowledgeable or competent personnel. In other words:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fztm46s5c6lyxor377e4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fztm46s5c6lyxor377e4x.png" alt="Alt Text" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our Bus Factor approach for our specific goal answers the question &lt;strong&gt;Which is the minimum number of contributors needed to make the 50% of git code on Jitsi?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a result, we get that in 2019, Jitsi only needed 2 developers to make the 50% of git code while in 2020 Jitsi needs at least 9 developers. This fact is indeed good for Jitsi’s project health!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flcv4ds4uzvu6u0vf11w8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flcv4ds4uzvu6u0vf11w8.png" alt="Alt Text" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see Jupyter Notebook query in our &lt;a href="https://gitlab.com/msdatascienceurjc/ELK-perceval" rel="noopener noreferrer"&gt;GitLab repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Work
&lt;/h2&gt;

&lt;p&gt;This is just a very introductory analysis to measure covid-19 impact on Jitsi. Of course, we can improve a lot of things. Some future work might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Improve author affiliation: We know an existing open source tooling called &lt;a href="https://github.com/chaoss/grimoirelab-sortinghat" rel="noopener noreferrer"&gt;SortingHat&lt;/a&gt;, that can be implemented with Perceval and other &lt;a href="https://github.com/chaoss/grimoirelab" rel="noopener noreferrer"&gt;GrimoireLab&lt;/a&gt; components. SortingHat is an identity affiliation management. Thus, identities corresponding to the same real person can be merged in the same unique identity with a unique uuid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filter “unknown” category within gender term: Maybe gender-guesser package is not the best one to use. Do you know other python packages we can use? Leave your comments below :) &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Work on new metric for risk assessment, such as the &lt;a href="https://en.wikipedia.org/wiki/Gini_coefficient" rel="noopener noreferrer"&gt;Gini coefficient&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And last but not least: &lt;strong&gt;Correlation does not imply causation&lt;/strong&gt;. We don’t have all the domain knowledge Jitsi maintainers and community specialists might have. A relationship between Covid-19 and contributor's activity might be just coincidence&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>git</category>
    </item>
    <item>
      <title>Remote work: The Open Source Way</title>
      <dc:creator>Ana Jimenez Santamaria</dc:creator>
      <pubDate>Fri, 13 Mar 2020 22:40:59 +0000</pubDate>
      <link>https://dev.to/anajsana95/remote-work-the-open-source-way-i52</link>
      <guid>https://dev.to/anajsana95/remote-work-the-open-source-way-i52</guid>
      <description>&lt;p&gt;Working remotely has become mission-critical now more than ever. Since one of the recommendations to stop COVID-19 massive spread is to stay at home, many companies are seeking effective strategies and tools for remote work.&lt;/p&gt;

&lt;p&gt;For many people, this situation could be an overwhelming first-time experience, but for many others this is not something new: People behind Open Source programs have been involved in remote collaborative workspaces since decades. How does the Open Source ecosystem manage remote workforce? &lt;/p&gt;

&lt;h2&gt;
  
  
  Everything starts with culture
&lt;/h2&gt;

&lt;p&gt;People is desperately asking for collaborative tools to stay productive and connected. But is that the best approach? &lt;/p&gt;

&lt;p&gt;Think about it, &lt;em&gt;Why giving to someone that has never handle remote work, a bunch of tools without context?&lt;/em&gt; In fact, tooling is the last thing you should be thinking of. Open Source has succeed over the past years not just because of tools, but because of culture&lt;/p&gt;

&lt;p&gt;Before going nuts with &lt;em&gt;what tools should people use&lt;/em&gt;, I suggest to first investigate &lt;em&gt;what strategies are used within open source communities&lt;/em&gt; and &lt;em&gt;what can companies learn from them&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This way of thinking can somehow relate to InnerSource term: a methodology which main goal is to establish &lt;em&gt;open source-like culture within organizations&lt;/em&gt;. It has a consolidated and active community, called &lt;a href="https://innersourcecommons.org/" rel="noopener noreferrer"&gt;InnerSource Commons&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Hands on remote-work: Best practices and suggestions
&lt;/h2&gt;

&lt;p&gt;In my company -Committed Open Source practitioners- working from home has never been a problem- We have been doing it since always!- This might be the reason why, some people from Twitterverse and Instagram asked me about best practices to follow in order to manage remote collaboration and productivity. Then I though that the best way to answer was by sharing &lt;em&gt;my top 6 best practices&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document everything:&lt;/strong&gt; As in person-meetings are not an option, internal documentation plays an important role to ensure collaboration. This is a really common practice within open source projects, as code is being build collaboratively and remotely, done by people from different time zones and cultures. Some of them are experienced contributors, others are just newbies trying to get into their first open source contribution. in both cases, they need to know &lt;em&gt;how to contribute&lt;/em&gt; - as well as pitfalls to avoid, guidelines, etc- and such information needs to be accessible and visible for everyone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build a Kanban board&lt;/strong&gt;: One of the main resources coming from Agile methodologies that can be applied for any situations that requires organizing and scheduling tasks. This is so helpfull that I'm not only use it at work to coordinate tasks with my team, but also with my classmates for my Data Science MS... Even for my personal projects!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fitck9tnxctfd54tbhb11.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fitck9tnxctfd54tbhb11.jpg" title="Kanban board" alt="Kanban board" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schedule daily meetings to keep track of current work:&lt;/strong&gt; This will help to keep your team synchronized, know what they are working at and find potential bottle necks... A good idea is to focus on short tasks, that can be done within 1 or 3 days period to track better the activity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set your own working hours and breaks:&lt;/strong&gt; set up an alarm and create a daily routine for work. The same way you do at work,dress up -avoid full-day pijama- and have specific clock-in and clock-out hours. This is very helpful to divide work vs spare time, and &lt;em&gt;not fall into an endless working state&lt;/em&gt; that at some point will lead into a &lt;em&gt;burnout&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/3o6Mblpo7jq1JBaXLi/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/3o6Mblpo7jq1JBaXLi/giphy.gif" title="time" alt="time" width="480" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Socialize:&lt;/strong&gt; working from home doesn't mean you can't have great coffee break conversations with your teammates!. At my company, Every Friday at 2:00pm, we have what we call &lt;em&gt;coffee-chats&lt;/em&gt; -. We join the call,  talk about our week, and enjoy a nice cup of coffee with friends!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/687qS11pXwjCM/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/687qS11pXwjCM/giphy.gif" title="Coffee friends" alt="Coffee time" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do Exercise + healthy diet:&lt;/strong&gt; I'm quite a restless person*, which means I need to remain focus and productive as much as possible if I want to do all the activities I've planned for that day. &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;*I have full-time work, Data Science MS, Japanese, and boxing classes, while saving time for my family and friends... All in the same week!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Boxing classes helped me a lot when covid-19 situation was not there, but now that gym is not the best place to be, I still doing some cardio at home to relieve stress. Moreover, avoiding added sugar and fast food intake made me feel less tired and more proactive during the day&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling
&lt;/h2&gt;

&lt;p&gt;Once you have define a way of collaborative work and clear daily routines, it's time to look at the resources you need to make that possible.&lt;/p&gt;

&lt;p&gt;There are lots of things you need to keep an eye on when choosing the best tool for you: Data Privacy, licenses, budget, scalability and more. However, taking into account that I'm not an expert on this field, I'll just enumerate some of my favorite tools I have been using during the past year at work or personal projects, grouped by different scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communication Apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://slack.com/" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;: similar to telegram, but more specialized for remote teams. you can have different channels which makes great to communicate among different working teams, topics or departments&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video chat&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://meet.jit.si/" rel="noopener noreferrer"&gt;Jitsi.meet&lt;/a&gt;: No log ins and completely free video conferencing. You know what's best? it's fully encrypted!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trello.com" rel="noopener noreferrer"&gt;Trello&lt;/a&gt;: really useful for those willing to use a &lt;a href="https://en.wikipedia.org/wiki/Kanban_(development)" rel="noopener noreferrer"&gt;Kanban&lt;/a&gt; board to manage daily tasks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File hosting services&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nextcloud.com/" rel="noopener noreferrer"&gt;NextCloud&lt;/a&gt;: Actually this is the only one I haven't use it yet, but I've heard a lot of positive feedback about this tool! Despite other alternatives, this solution does NOT store your data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Podcast/interview recording&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://etercast.com/" rel="noopener noreferrer"&gt;Etercast&lt;/a&gt; If you're planing to have several guests for an interview, Etercast might be a good solution. No log in, free and very easy to manage&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: &lt;a href="http://jgbarah.github.io/Notes/foss-distance-learning.html" rel="noopener noreferrer"&gt;This post by Jesús González-Barahona&lt;/a&gt; shows more resources, also applied for &lt;em&gt;distance learning&lt;/em&gt;. The best thing is that all of them are entirely free, open source software (FOSS)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Not every business is suitable for remote work: My goal here is not to show that everyone can work remotely -cause we can't-, but to give some guidance to those few who &lt;em&gt;potentially can&lt;/em&gt; but &lt;em&gt;don't know how to start with&lt;/em&gt; and share my thoughts and experience working in remote and collaborative environments.&lt;/p&gt;

&lt;p&gt;Thanks for reading until the end. For those experienced people in collaborative and remote environments, I'd love to see any suggestions or tools that you might find useful to share to folks who are facing remote work for their first time. &lt;/p&gt;

&lt;p&gt;Feedback is more than welcome :)&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>remotework</category>
    </item>
  </channel>
</rss>
