<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Sachin Pal on Medium]]></title>
        <description><![CDATA[Stories by Sachin Pal on Medium]]></description>
        <link>https://medium.com/@geekpython?source=rss-98e733987320------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*kxKxyiluFTWmw0CxTr9i2g.png</url>
            <title>Stories by Sachin Pal on Medium</title>
            <link>https://medium.com/@geekpython?source=rss-98e733987320------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 19 May 2026 15:12:20 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@geekpython/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How to Authenticate AI Agents in B2B SaaS: Delegated Auth, Scoped Tokens, and Audit Trails]]></title>
            <link>https://geekpython.medium.com/how-to-authenticate-ai-agents-in-b2b-saas-delegated-auth-scoped-tokens-and-audit-trails-3b7b31f97878?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/3b7b31f97878</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[auth]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Sat, 21 Mar 2026 10:04:34 GMT</pubDate>
            <atom:updated>2026-03-21T10:04:34.870Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Iumiwj1ix3nw28dXGr_0sg.png" /></figure><p>Let’s start with a scenario that should sound familiar.</p><p>You’ve shipped an AI agent inside your B2B SaaS product. It summarizes meetings, drafts content, creates notes in Notion, and manages knowledge workflows — all on behalf of your users. It’s fast. It’s delightful. Your customers love it.</p><p>Now ask yourself: when your agent creates a Notion page on behalf of John from the XCorp account — does Notion’s API actually know it’s John? Does it know it’s XCorp? Does it know the agent is only supposed to write to specific workspaces and not read everything John has ever written?</p><p>If your answer involves a shared API key, a service account with broad permissions, or a vague “we trust the agent to behave” — this article is for you.</p><h3>How Most Teams Handle Agent Auth Today (And Why It’s a Liability)</h3><p>Most teams building customer-facing AI agents have stitched together authentication in one of three ways. They all work. They all have problems that don’t show up until something goes very wrong.</p><h4><strong>Pattern 1: The Hardcoded API Key</strong></h4><p>This is the fastest path from prototype to production, which is exactly why it’s so common. Your agent needs to create Notion pages, read databases, manage workspaces — you grab a Notion integration token, stuff it in an environment variable, and ship it.</p><p>The token is tied to a single Notion integration. It has whatever permissions that integration has. It has no concept of which user triggered the action. It has no concept of which org the request is for. It will keep working indefinitely, or until someone revokes the integration and everything breaks.</p><blockquote><em>A hardcoded API key is like giving your delivery driver a master key to every apartment in the building because you don’t want to bother them with individual unit codes.</em></blockquote><p>There’s no revocation per user. There’s no scoping per tenant. And if someone asks “which agent action ran on behalf of John from Sales at XCorp last Tuesday?” the answer is a spreadsheet of server logs and a prayer.</p><h4><strong>Pattern 2: The Shared Service Account</strong></h4><p>A step up from a raw API key, and somehow worse in practice. A single Notion integration with broad workspace access handles every agent action, for every user, for every org.</p><p>The downstream system sees requests from one identity. You’ve lost all user context the moment the agent touches an external tool. If you’re in a multi-tenant environment — and if you’re building B2B SaaS, you are — this means your agent is running as a single undifferentiated identity across every customer organization you have.</p><p>That’s not multi-tenancy. That’s a liability waiting to manifest.</p><h4><strong>Pattern 3: The “Trust the Agent” Architecture</strong></h4><p>This one is the most optimistic. You trust the application layer to enforce access controls. The agent figures out from context which user it’s acting for, and you trust it won’t do anything it shouldn’t.</p><p>This works precisely until it doesn’t. An edge case in the LLM’s context handling. A subtly malformed request. A prompt injection attack that convinces the agent it’s acting for a different user. You have no cryptographic guarantee of identity at the infrastructure level — just vibes and hope.</p><h3>The Multi-Tenancy Time Bomb</h3><p>Here’s the failure mode that keeps B2B SaaS engineering leads up at night. Not a dramatic breach. A quiet data bleed.</p><p>Imagine your AI agent is helping a content manager at TechCorp. The agent creates a Notion page and starts writing to a database. Somewhere in the permission model, there’s a misconfiguration, the agent’s shared integration token has access to a Notion workspace that doesn’t properly filter by organization. Because the agent doesn’t carry a user-scoped token, Notion’s API has no way to enforce tenant boundaries at the credential level.</p><p>The agent writes data. Some of it might land in the wrong workspace.</p><p>This isn’t a hypothetical. It’s a class of bugs that shows up in any system where authorization decisions are made at the application layer rather than the infrastructure layer. Agents make it worse because they operate autonomously, at speed, across many tenants, often without a human in the loop to notice when something looks off.</p><p>Tenant isolation isn’t a nice-to-have in B2B SaaS. It’s the whole point. Every enterprise customer assumes it’s airtight. If your agent auth model relies on application-level trust rather than cryptographically verified, tenant-scoped tokens, you have a gap.</p><h3>What Delegated, Scoped, Auditable Agent Auth Actually Looks Like</h3><h3>The Identity Problem, Stated Cleanly</h3><p>When a human logs into your app, authentication is relatively straightforward. They prove who they are, you issue a session token tied to their identity, and every downstream request carries that identity as a cryptographic claim. The system knows who is asking, what they’re allowed to see, and can log it against their account.</p><p>An AI agent acting on a user’s behalf introduces a new actor into this chain. The agent isn’t the user. It’s not your service. It’s something operating in between, and classical auth models weren’t designed for it.</p><p>What you actually need is <strong>delegated authorization</strong>: the user explicitly grants the agent permission to act on their behalf, the agent receives a token that carries the user’s identity and org context, and that token is scoped to exactly the actions the user authorized. The downstream system doesn’t trust the agent, it trusts the token, which was issued through a consent flow the user approved.</p><blockquote><em>The question isn’t </em><strong><em>“does the agent have permission to do this?”</em></strong><em> It’s </em><strong><em>“did this specific user, in this specific org, authorize this agent to do exactly this?”</em></strong></blockquote><h3>What Delegated Auth Actually Looks Like in Practice</h3><p>The pattern is OAuth, specifically, OAuth flows adapted for the agent context. Here’s the mental model:</p><ul><li>A user interacts with your product and authorizes your agent to access external tools on their behalf.</li><li>Your system initiates an OAuth flow that ties the resulting token to that specific user, in their specific org, with a specific set of scopes.</li><li>The agent receives this token and uses it when making downstream API calls. The token carries the user’s identity. The downstream system can verify it cryptographically.</li><li>If the user revokes consent, the token is immediately invalidated. The agent loses access at the exact moment authorization is withdrawn.</li></ul><p>This is fundamentally different from a service account. The agent doesn’t accumulate permissions over time. It has exactly what this user authorized for exactly this session, and nothing more.</p><h3>Least Privilege, Per Tenant, Per Action</h3><p>Token scoping is the mechanism that makes least privilege real at the agent layer. Instead of issuing a token that says “this agent can do anything this user can do,” you issue a token that says “this agent can create Notion pages in approved workspaces, on behalf of this user, in this org, until revoked.”</p><p>This matters in practice for a few reasons. Agents make mistakes. LLMs hallucinate. Prompt injection is a real attack vector. A scoped token acts as a hard boundary, even if the agent logic goes sideways, the token can’t be used to take actions it was never authorized to take. It’s defense in depth at the identity layer.</p><p>In a multi-tenant context, token scoping also means every token is inherently org-scoped. There’s no way for a token issued on behalf of John at XCorp to accidentally write into TechCorp’s Notion workspace. The tenant boundary is baked into the credential, not enforced by application logic you might forget to write.</p><h3>What a Token Should Actually Carry</h3><p>A properly constructed agent token is typically a short-lived OAuth access token backed by a JWT. Here’s what the payload should look like:</p><pre>{<br>  &quot;sub&quot;: &quot;user_01HXYZ123&quot;,          // stable user identifier — not email, not name<br>  &quot;org_id&quot;: &quot;org_xcorp_456&quot;,        // tenant context, baked in at issuance<br>  &quot;scope&quot;: &quot;notion:pages:write notion:workspace:read-specific&quot;,<br>  &quot;agent_id&quot;: &quot;agent_content_mgr&quot;,  // which agent received this delegation<br>  &quot;auth_event_id&quot;: &quot;authz_789&quot;,     // reference to the consent event that produced this token<br>  &quot;iat&quot;: 1714000000,<br>  &quot;exp&quot;: 1714003600                 // 1 hour; vault handles refresh, not your app<br>}</pre><p>Each field is doing specific work.</p><p>sub makes every downstream action attributable to an individual — not a service account, not a role.</p><p>org_id encodes the tenant boundary in the credential itself rather than relying on application logic to filter correctly.</p><p>scope constrains what the agent can do at the infrastructure layer, so even a misbehaving agent can&#39;t exceed what the user approved.</p><p>auth_event_id is the key to clean revocation: revoke that authorization event, and every token derived from it is immediately invalid, regardless of whether it&#39;s technically unexpired. The vault tracks this mapping — your application doesn&#39;t have to.</p><p>The short exp limits blast radius if a token leaks. The vault handles silent refresh — your application always gets a live token via get_connected_account, never needs to detect expiry or trigger a refresh cycle itself.</p><h3>Revocation</h3><p>This one gets underweighted in most agent auth discussions. With a hardcoded API key or a service account, revocation is a blunt instrument — you revoke the key, everything breaks, you scramble to reissue and reconfigure. With delegated tokens tied to per-user authorization events, revocation is precise: the user withdraws consent, that token is invalidated, the agent loses access for exactly that user and no one else. Nothing else in your system is disrupted.</p><p>This is the property that makes enterprise customers comfortable. They want to be able to say “if I change my mind, I can revoke access in one click”. Delegated OAuth gives you that. Service accounts do not.</p><h3>The Audit Trail You’ll Actually Need</h3><p>At some point, a customer is going to ask you what your agent did. Maybe it’s a compliance audit. Maybe something went wrong and they’re trying to figure out what happened. Maybe their InfoSec team just wants to see the logs before they renew.</p><p>With delegated, scoped auth, answering this question becomes straightforward. Every token carries user identity, org context, and scope. Every action the agent takes is traceable to a specific authorization event — when the user approved it, what they approved, and whether that authorization is still valid.</p><p><strong>“What did the agent do on behalf of John from Sales at XCorp last Tuesday?”</strong> becomes a query, not an investigation.</p><h3>Implementing This Pattern</h3><p>Rolling delegated agent auth from scratch is doable — you’re essentially building an OAuth authorization server with a token vault, per-tenant scoping logic, rotation policies, and audit hooks. Most teams building on top of this pattern reach for an existing implementation rather than owning that infrastructure, especially when it needs to be multi-region, SOC 2 compliant, and available at 99.99%.</p><p>The example below uses <a href="https://scalekit.com/agent-auth">Scalekit’s Agent Auth</a>, which handles the OAuth flows, token vault, rotation, and audit logging described above. The pattern is identical whether you use Scalekit or build it yourself — the goal is to make the mechanics concrete.</p><h3>A Python Example: Creating a Notion Page on Behalf of a User</h3><p>This example walks through a minimal but complete implementation of the full flow: user consent, token issuance, scoped API call, no hardcoded keys.</p><p><strong>Set Up Your Environment</strong></p><p>Get your API credentials from the Scalekit dashboard at scalekit.com → Developers → Settings → API Credentials, then install the SDK:</p><pre>pip install scalekit-sdk-python python-dotenv requests</pre><p>Initialize the client:</p><pre>import scalekit.client<br>import os<br>import requests<br>from dotenv import load_dotenv<br><br>load_dotenv()<br><br>scalekit_client = scalekit.client.ScalekitClient(<br>    client_id=os.getenv(&quot;SCALEKIT_CLIENT_ID&quot;),<br>    client_secret=os.getenv(&quot;SCALEKIT_CLIENT_SECRET&quot;),<br>    env_url=os.getenv(&quot;SCALEKIT_ENV_URL&quot;),<br>)<br>actions = scalekit_client.actions</pre><p><strong>Step 1: Authenticate the User</strong></p><p>Before the agent can create anything on the user’s behalf, the user needs to authorize access. Call get_authorization_link to generate a Notion OAuth consent URL — Scalekit handles the entire flow from here: getting the token, storing it in the vault, and refreshing it automatically when it expires.</p><pre># Generate Notion OAuth consent link for this user<br>link_response = actions.get_authorization_link(<br>    connection_name=&quot;notion&quot;,<br>    identifier=&quot;Sachin&quot;  # Unique identifier for this user in your system<br>)<br><br>print(f&quot;Click this link to authorize Notion: {link_response.link}&quot;)<br>input(&quot;Press Enter after completing authorization...&quot;)</pre><blockquote><strong>Note:</strong> Make sure the notion connection is set up in your Scalekit dashboard under Agent Actions → Connections → Create Connection. Use Scalekit&#39;s built-in credentials for faster development, no need to create your own Notion OAuth app.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RzdsgGvwOaEOh8D4IWKprw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8mFV2MPX7U6GwEIxf-G5OA.png" /></figure><p>The user clicks the link, sees Notion’s standard OAuth consent screen, approves access, and that’s it. They’ve defined what the agent is allowed to do. Everything from here is bounded by that decision.</p><p><strong>Step 2: Fetch the OAuth Token</strong></p><p>Now that the user has authorized, retrieve a fresh, valid access token. Scalekit keeps it refreshed in the background — you always get a live token without managing refresh cycles yourself.</p><pre># Retrieve the connected account to get the current OAuth token<br>response = actions.get_connected_account(<br>    connection_name=&quot;notion&quot;,<br>    identifier=&quot;Sachin&quot;<br>)<br>connected_account = response.connected_account<br><br># Extract the access token<br>tokens = connected_account.authorization_details[&quot;oauth_token&quot;]<br>access_token = tokens[&quot;access_token&quot;]</pre><p>This token is the user’s delegated identity. It’s scoped to exactly what they approved in the Notion consent screen — their workspaces, their pages, nothing else.</p><p><strong>Step 3: Create a New Notion Page</strong></p><p>Now use the token to call Notion’s API and create a page. Notion’s backend sees this request as the authorized user — their workspace, their permissions, no one else’s.</p><pre>headers = {<br>    &quot;Authorization&quot;: f&quot;Bearer {access_token}&quot;,<br>    &quot;Content-Type&quot;: &quot;application/json&quot;,<br>    &quot;Notion-Version&quot;: &quot;2022-06-28&quot;,  # Always pin the Notion API version<br>}<br><br># Find an accessible parent page to create under<br>search_response = requests.post(<br>    &quot;https://api.notion.com/v1/search&quot;,<br>    headers=headers,<br>    json={<br>        &quot;filter&quot;: {&quot;value&quot;: &quot;page&quot;, &quot;property&quot;: &quot;object&quot;},<br>        &quot;page_size&quot;: 1<br>    }<br>)<br>results = search_response.json().get(&quot;results&quot;, [])<br><br>if not results:<br>    print(&quot;❌ No accessible pages found. Make sure the user shared a page with the integration.&quot;)<br>else:<br>    parent_page_id = results[0][&quot;id&quot;]<br><br>    # Create a new page under the found parent<br>    create_response = requests.post(<br>        &quot;https://api.notion.com/v1/pages&quot;,<br>        headers=headers,<br>        json={<br>            &quot;parent&quot;: {&quot;page_id&quot;: parent_page_id},<br>            &quot;properties&quot;: {<br>                &quot;title&quot;: {<br>                    &quot;title&quot;: [{&quot;text&quot;: {&quot;content&quot;: &quot;Page Created by AI Agent 🤖&quot;}}]<br>                }<br>            },<br>            &quot;children&quot;: [<br>                {<br>                    &quot;object&quot;: &quot;block&quot;,<br>                    &quot;type&quot;: &quot;paragraph&quot;,<br>                    &quot;paragraph&quot;: {<br>                        &quot;rich_text&quot;: [<br>                            {<br>                                &quot;type&quot;: &quot;text&quot;,<br>                                &quot;text&quot;: {<br>                                    &quot;content&quot;: &quot;This page was created by an AI agent on behalf of Sachin using Scalekit Agent Auth. The agent had exactly the permissions this user approved — nothing more.&quot;<br>                                }<br>                            }<br>                        ]<br>                    }<br>                }<br>            ]<br>        }<br>    )<br><br>    new_page = create_response.json()<br>    print(f&quot;\n✅ Notion page created successfully!&quot;)<br>    print(f&quot;Page ID  : {new_page.get(&#39;id&#39;)}&quot;)<br>    print(f&quot;Page URL : {new_page.get(&#39;url&#39;)}&quot;)<br>    print(f&quot;Created  : {new_page.get(&#39;created_time&#39;)}&quot;)</pre><p>Notion’s backend sees this request as the authorized user — their workspaces, their permissions, scoped to exactly what they approved. The agent cannot access private pages, other users’ workspaces, or anything outside the approved scope. That constraint isn’t enforced by application logic — it’s encoded in the credential itself.</p><h4>No Raw APIs? Use Scalekit’s Built-in Tools</h4><p>If you’d rather not manage Notion’s API directly, Scalekit ships pre-built connectors for supported services. The same flow using Scalekit’s built-in tools:</p><pre>import scalekit.client<br>import os<br>from dotenv import load_dotenv<br><br>load_dotenv()<br><br># Initialize Scalekit client using environment variables<br>scalekit_client = scalekit.client.ScalekitClient(<br>    client_id=os.getenv(&quot;SCALEKIT_CLIENT_ID&quot;),<br>    client_secret=os.getenv(&quot;SCALEKIT_CLIENT_SECRET&quot;),<br>    env_url=os.getenv(&quot;SCALEKIT_ENV_URL&quot;),<br>)<br>actions = scalekit_client.actions<br><br># Generate Notion OAuth consent link for this user<br>link_response = actions.get_authorization_link(<br>    connection_name=&quot;notion&quot;,<br>    identifier=&quot;Sachin&quot;<br>)<br>print(f&quot;Click this link to authorize Notion: {link_response.link}&quot;)<br>input(&quot;Press Enter after completing authorization...&quot;)<br><br># Step 1: Use Scalekit&#39;s built-in tool to search for existing Notion pages<br># No raw API call needed — Scalekit handles auth injection internally<br>search_response = actions.execute_tool(   # &lt;-- Scalekit tool call<br>    tool_name=&quot;notion_page_search&quot;,       # &lt;-- built-in Notion tool<br>    identifier=&quot;Sachin&quot;,                  # &lt;-- user identity passed here<br>    tool_input={<br>        &quot;page_size&quot;: 3,<br>        &quot;sort_direction&quot;: &quot;descending&quot;,<br>        &quot;sort_timestamp&quot;: &quot;last_edited_time&quot;<br>    }<br>)<br><br># Response data lives in search_response.data — no json.loads() needed<br>results = search_response.data.get(&quot;results&quot;, [])<br>print(f&quot;Pages found: {len(results)}&quot;)<br><br>if not results:<br>    print(&quot;❌ No pages found. Please share a page on the Notion consent screen.&quot;)<br>else:<br>    parent_page_id = results[0][&quot;id&quot;]<br>    print(f&quot;✅ Parent page found: {parent_page_id}&quot;)<br><br>    # Step 2: Use Scalekit&#39;s built-in tool to create a new Notion page<br>    new_page_response = actions.execute_tool(   # &lt;-- Scalekit tool call<br>        tool_name=&quot;notion_page_create&quot;,         # &lt;-- built-in Notion tool<br>        identifier=&quot;Sachin&quot;,<br>        tool_input={<br>            &quot;parent_page_id&quot;: parent_page_id,   # &lt;-- from Step 1 result<br>            &quot;properties&quot;: {<br>                &quot;title&quot;: {<br>                    &quot;title&quot;: [{&quot;text&quot;: {&quot;content&quot;: &quot;Page Created by AI Agent 🤖&quot;}}]<br>                }<br>            },<br>            &quot;child_blocks&quot;: [<br>                {<br>                    &quot;object&quot;: &quot;block&quot;,<br>                    &quot;type&quot;: &quot;paragraph&quot;,<br>                    &quot;paragraph&quot;: {<br>                        &quot;rich_text&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: {<br>                            &quot;content&quot;: &quot;This page was created by an AI agent on behalf of Sachin using Scalekit Agent Auth. The agent had exactly the permissions this user approved — nothing more.&quot;<br>                        }}]<br>                    }<br>                }<br>            ],<br>            &quot;notion_version&quot;: &quot;2022-06-28&quot;<br>        }<br>    )<br><br>    new_page = new_page_response.data   # &lt;-- response data directly accessible<br>    print(f&quot;\n✅ Notion page created successfully!&quot;)<br>    print(f&quot;Page ID  : {new_page.get(&#39;id&#39;)}&quot;)<br>    print(f&quot;Page URL : {new_page.get(&#39;url&#39;)}&quot;)<br>    print(f&quot;Created  : {new_page.get(&#39;created_time&#39;)}&quot;)</pre><p>execute_tool handles auth injection internally — Scalekit looks up the connected account for that identifier, retrieves the live token from the vault, and injects it into the request. The response comes back as a Python dict. No token handling, no JSON parsing, no API version management.</p><p>Available tools for Notion include notion_page_search, notion_page_create, notion_database_create, notion_database_query, notion_comment_create, and more. Every supported connector has its own tool set — check the full list <a href="https://scalekit.com/agent-connector/notion">here</a>.</p><h4><strong>What Actually Happens When You Run This</strong></h4><p>No magic, no hand-waving. Here’s exactly what the flow looks like end to end once you run the program:</p><p><strong>1. You run the program</strong></p><p>Your terminal prints a Scalekit-generated authorization link and waits. The program is paused, nothing happens until you say so.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LVObH0NmI8X6BxENdTSTEg.png" /></figure><p><strong>2. You open the link — Scalekit shows an authorization interface</strong></p><p>You follow the link in your browser. Scalekit presents a clean authorization screen that tells you exactly what the agent is asking for — which app (Notion), which permissions, on whose behalf. No fine print. No vague “access to everything.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/756/1*xxY4cleDAVKy-wBFFXFZuw.png" /></figure><p>You click <strong>Allow access</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/756/1*sYGsI_9WkiDDzAQwK-aXdg.png" /></figure><p><strong>3. Notion’s OAuth flow completes in the background</strong></p><p>Scalekit handles the entire OAuth handshake with Notion — exchanges the auth code for an access token, stores it securely in the token vault, and marks the connected account for identifier=&quot;Sachin&quot; as ACTIVE. You don&#39;t see any of this. It just works.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/1*WS7C073fTGYPNkg239-ZOQ.png" /></figure><p><strong>4. You come back to the terminal and press Enter</strong></p><p>The program resumes, fetches the live token from the vault, and moves on to the Notion API call.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RHuw2sHQA3p3YgMJ5gzUog.png" /></figure><p><strong>5. The agent creates a page in Notion on your behalf</strong></p><p>You access the page URL. A new page is sitting right there, created by the agent, using your delegated identity, with exactly the permissions you approved and nothing more.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LZLO6Q9-4KoM8hzE9bQUfw.png" /></figure><p>The agent acted as you. Notion knew it was you. And you stayed in control the whole time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rQdpW_hipFOY-juQYyGBBg.png" /></figure><h3>Conclusion</h3><p>Agents are no longer just internal tools, they’re acting on behalf of real users, inside real organizations, touching real data. A hardcoded API key got you to the demo. It won’t get you to production.</p><p>The pattern is simple: delegated auth, scoped tokens, full audit trail. Scalekit gives you the infrastructure to do it right, without rebuilding your entire auth stack.</p><p>Your users are trusting your agent with their data. Make sure it’s worthy of that trust.</p><p><strong>That’s all for now.</strong></p><p><strong>Keep Coding✌️✌️.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3b7b31f97878" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Built a Full Stack App Using Just Prompts — And It Actually Worked]]></title>
            <link>https://geekpython.medium.com/i-built-a-full-stack-app-using-just-prompts-and-it-actually-worked-25fffc9c8d97?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/25fffc9c8d97</guid>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[full-stack]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 17:20:49 GMT</pubDate>
            <atom:updated>2026-03-16T17:20:49.389Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P1Z6JEq49oCLn42HzD3d4A.png" /></figure><p>Okay so I need to start this with some honesty.</p><p>I have started, and abandoned, more side projects than I can count. Not because I ran out of ideas, not because I got bored. Every single time, it was the same thing. The backend.</p><p>I would get an idea, get excited, open my editor, start building the frontend, and everything would feel great for maybe an hour. Then I’d realize I need users to log in. Which means I need auth. Which means I need a database. Which means I need to figure out hosting, connection strings, environment variables, migrations, and suddenly I’m reading a Stack Overflow answer from 2019 wondering if it’s still relevant.</p><p>The idea doesn’t die dramatically. It just slowly suffocates under the weight of infrastructure.</p><p>And look, I know some of you reading this are backend engineers who genuinely enjoy this stuff. Good for you, seriously. But for the rest of us, people who just want to build things that work, the backend has always been this annoying tax you pay before you’re allowed to do the thing you actually wanted to do.</p><p>So when I heard about InsForge and how it’s designed specifically for AI coding agents to handle the backend automatically, I was curious but skeptical. I’ve been burned by “it just works” promises before. But I tried it anyway, built a real app with it, and here’s everything that happened — the good parts, the broken parts, all of it.</p><h3>Let’s Talk About Why Backend Development Is Actually Hard</h3><p>I don’t want to just skim past this part because I think it’s important. When people say “the backend is hard,” they’re usually vague about <em>what specifically</em> is hard. Let me be specific.</p><h3>Problem 1: Decision Paralysis Before You’ve Written One Line</h3><p>You decide you need a database. Cool. Now which one?</p><p>Postgres is the safe, serious choice. MySQL is what a lot of tutorials use. MongoDB is document-based and people swear by it for flexibility. SQLite is great for local dev but gets complicated in production. There’s also PlanetScale, Turso, Neon, CockroachDB — all Postgres-compatible but with different tradeoffs around scaling and pricing.</p><p>You spend 45 minutes reading comparison articles. Each one recommends something different. You pick Postgres because it seems the most “serious.” Now where do you host it? Railway? Supabase? Render? AWS RDS? Neon? Each has a free tier with different limitations. Each has slightly different connection string formats. You pick one.</p><p>You haven’t written a single line of application code yet. You’ve been at this for an hour.</p><h3>Problem 2: Authentication Is a Trap</h3><p>Authentication looks simple from the outside. Users log in. Users log out. How hard can it be?</p><p>Very hard, as it turns out.</p><p>First, you decide on an approach. Sessions or JWT tokens? Sessions are simpler to reason about but require server-side state. JWT tokens are stateless but then how do you handle logout properly? If you just let the token expire, a logged-out user can still use that token until it expires. You need a token blacklist, which requires more database infrastructure.</p><p>Okay, let’s say you pick sessions. Now you need to hash passwords. bcrypt? argon2? You use bcrypt because most tutorials do. You implement registration, you implement login, it works locally.</p><p>Now you add email verification. You need to send emails, which means you need an email provider such as Sendgrid, Resend, Postmark, Amazon SES. You pick one, you set up API keys, you write the email templates, you implement the verification token logic, you store tokens in the database with expiry times, you write the endpoint that handles the verification click.</p><p>Registration now works end to end. You test login. It works. You test logout. It works. You test “what happens if someone tries to log in with an unverified email” and your app throws an uncaught exception and crashes.</p><p>You fix it. You add Google OAuth because users expect it. OAuth has its own entire complexity surface like redirect URIs, client IDs, callback handling, merging OAuth accounts with existing email accounts if the same email was used for both.</p><p>You’ve been working for two days and you haven’t built a single feature of your actual app.</p><h3>Problem 3: File Storage Is Deceptively Annoying</h3><p>Your app needs to let users upload a profile photo. Or attach an image to a post. Or upload a document. Seems simple.</p><p>You need cloud storage. S3 is the standard. You create an AWS account, create a bucket, figure out the IAM permissions system (which has its own learning curve), generate access keys, configure CORS so your frontend can talk to S3, implement presigned URLs so you’re not exposing your credentials to the client, write the upload logic, write the delete logic, handle errors when uploads fail.</p><p>Alternatively you use Supabase Storage or Cloudflare R2, which are simpler, but you still need to understand the permission model, configure buckets, write the integration code.</p><p>After all that, file uploads work. But now when you test on a slow connection, you notice there’s no progress indicator, and if the upload fails halfway through, the user gets no feedback. You go back and implement that too.</p><h3>Problem 4: Local Works, Production Doesn’t</h3><p>The worst moment in any project. You’ve built everything locally. It all works. You deploy it to Vercel, or Railway, or wherever and something is broken.</p><p>The database connection string is different in production. The environment variables aren’t loading correctly. The file upload URLs are using localhost. CORS is blocking requests because you forgot to add your production domain to the allowed origins. Email verification links are pointing to localhost.</p><p>You spend three hours debugging things that have nothing to do with your actual application and everything to do with the gap between your local environment and production.</p><h3>Problem 5: Keeping Everything in Sync</h3><p>As your app grows, you add a new feature that requires a new database column. Now you need to write a migration. But your production database already has data in it, so you need to write the migration carefully so it doesn’t break existing records. You test it locally, it works. You run it in production, something unexpected happens.</p><p>Or you change your authentication logic and now sessions from before the change are invalid, and users are getting logged out randomly, and you don’t fully understand why until you spend an afternoon tracing through the session handling code.</p><p>None of this is insurmountable. Experienced backend developers handle all of this routinely. But if you’re someone who just wants to build an app, if backend infrastructure is a means to an end rather than the thing itself then this is a brutal amount of overhead before you get to do anything interesting.</p><h3>Enter AI Coding Agents — And Their Limitations</h3><p>AI coding agents like Cursor and Claude Code have helped a lot with this. The experience of describing what you want and watching code appear is genuinely remarkable. For frontend work especially — React components, UI logic, state management — these tools are fantastic.</p><p>But they struggle with backend setup in a specific, frustrating way.</p><p>The problem is that AI agents are working from training data, and backend infrastructure tools update constantly. Supabase changes their SDK. AWS updates their SDK. Authentication libraries deprecate methods. The agent writes code based on what it knows, which might be six months out of date, and then you spend an hour debugging why the auth isn’t working only to realize the agent used a method that was deprecated in the last SDK update.</p><p>Beyond that, setting up backend services involves a lot of clicking through dashboards, configuring settings in UIs, copying values from one place to another. AI agents can’t do that — they can only write code. So the setup process still has a significant manual component, and the seams between “what the agent wrote” and “what you set up manually” are often where bugs hide.</p><p>What would actually solve this is a backend platform that’s built from the ground up for AI agents. Not a platform that has an AI feature bolted on, but one where the AI agent is the primary user — where every capability is exposed as a tool the agent can call directly, with documentation structured for machine consumption rather than human reading.</p><p>That’s InsForge.</p><h3>What InsForge Actually Is</h3><p>InsForge calls itself <strong>“the backend for agentic development”</strong>. The key word there is agentic, it’s designed specifically to be used by AI coding agents, not human developers clicking through dashboards.</p><p>Here’s what you get with every InsForge project:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wLa7wJ_ostdKJKXx5P3xSQ.png" /></figure><p><strong>Postgres Database</strong> — A proper relational database, not a toy. Production-grade Postgres, ready immediately when you create a project. The agent can create tables, run queries, manage relationships — all through native MCP tools, not by writing raw SQL and hoping it runs.</p><p><strong>Authentication</strong> — Full user management including email/password signup, email verification, session management, and OAuth providers including Google. The kind of auth that represents days of work to implement correctly from scratch.</p><p><strong>Cloud Storage</strong> — File storage for images, documents, anything your app needs to store. No S3 configuration, no IAM policies, no presigned URL boilerplate. The agent can create buckets and upload files through the same tool interface as everything else.</p><p><strong>Edge Functions</strong> — Serverless backend logic that runs globally. Email sending, webhook handling, scheduled tasks, anything that needs to run server-side without a dedicated server.</p><p><strong>Realtime</strong> — Live data subscriptions. If you want your app to update automatically when data changes — think collaborative features, live dashboards, notifications, it’s built in.</p><p><strong>AI Model Gateway</strong> — Built-in access to AI models if your app needs them. No separate API keys to manage.</p><p><strong>Deployment</strong> — Ship your app through InsForge without needing a separate hosting platform.</p><p>The reason this matters more than just having a good feature list is the way these features are exposed. Every single capability is available as a native MCP (Model Context Protocol) tool. Your AI agent doesn’t read documentation and write API calls, it directly invokes tools. Create a database table. Set up email auth. Create a storage bucket. These are direct function calls the agent makes against InsForge’s infrastructure.</p><p>The practical result of this is measurable. According to benchmarks run at mcpmark.ai, AI agents using InsForge are <strong>1.6x faster</strong> than with Supabase, use <strong>30% fewer tokens</strong>, and achieve nearly <strong>double the accuracy</strong>, means the agent gets things right the first time significantly more often, which means fewer debugging loops, which means you actually ship things.</p><h3>Setting Up InsForge With Cursor: Every Step</h3><p>Let me walk through the setup in detail, because it’s actually much simpler than I expected and I want you to see exactly what it looks like.</p><h4>Step 1: Create Your Account and Project</h4><p>Go to <a href="https://insforge.dev">insforge.dev</a> and sign up. Once you’re in, you’ll see your organization dashboard. Click to create a new project and give it a name that reflects what you’re building.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GFXJIJk9iM0NTGWxIKA3Vg.png" /></figure><p>When InsForge creates your project, it’s actually provisioning a real Postgres database, configuring authentication infrastructure, and setting up storage, all in the background while you’re looking at the UI. By the time the loading spinner is done, you have a complete backend environment waiting for you.</p><p>Compare this to the alternative: spinning up a Postgres instance on Railway, then going to Supabase for auth, then configuring S3 for storage, then figuring out how to make all three talk to each other. InsForge does that entire setup in about ten seconds.</p><h4>Step 2: Connect Your Editor</h4><p>Navigate to the Terminal section in the InsForge dashboard. You’ll see a list of supported editors such as Cursor, Claude Code, GitHub Copilot, Cline, Windsurf, and more.</p><p>Click on Cursor. Then click <strong>“Add to Cursor”</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zxxUUYKqXIVpJLwoDEhCOQ.png" /></figure><p>Cursor opens, and every field in the MCP configuration is already populated. The server URL, the authentication credentials, the project identifier — all filled in without you doing anything. You just confirm the installation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/845/1*gX_yo085axOjzP6cM1RoMA.png" /></figure><p>If you’re using a different editor like VS Code, Claude Code, anything else, InsForge recommends the extension method instead of the direct MCP approach.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BhADM9g8n1Zm1qxZLDRJhA.png" /></figure><h4>Step 3: Verify the Connection</h4><p>Once installed, open Cursor and run this prompt:</p><pre>I&#39;m using InsForge as my backend platform, call InsForge MCP&#39;s fetch-docs tool to learn about InsForge instructions.</pre><p>InsForge will confirm the connection and come back with a setup guide. But here’s the part I didn’t expect — along with the confirmation, it gives you three steps. And those three steps aren’t just “you’re good to go.” They’re actually instructions for setting up InsForge for your frontend as well.</p><p>So if you take those three steps and give them back to the agent as a prompt — just paste them in — your frontend will also be wired up to InsForge automatically. No extra configuration, no hunting through docs. Backend and frontend, both connected, from a single setup flow.</p><h3>Building the App: Everything That Happened</h3><p>Now for the actual build. I’m going to be honest about the process, including the things that didn’t work perfectly the first time, because I think sanitized tutorials where everything works on the first try are actually misleading and not that helpful.</p><h4>Writing the Prompt</h4><p>Before typing anything, I spent a few minutes thinking through what the app needed to do. This is worth doing, a vague prompt produces a vague app, and you’ll waste iteration cycles fixing things that could have been specified upfront.</p><p>I decided to build a calories and hydration tracking app called FitTrack. Here’s the exact prompt I used:</p><pre>Build a lightweight Diet &amp; Hydration Tracker app named &quot;Fittrack&quot; using InsForge as the backend platform with these features:<br>- User profiles with basic details (age, weight, height, activity level)<br>- Daily meal tracking with meal type (breakfast, lunch, dinner, snacks) and calories<br>- Custom food items with calories, macros, and portion size<br>- Water intake tracking with daily hydration goals<br>- Quick add buttons for water intake (250ml, 500ml, custom)<br>- Daily summary showing total calories consumed vs target<br>- Hydration progress indicator for the day<br>- History view to track meals and water intake by date<br>- Notes for each day (cheat day, fasting, workout, etc.)<br>- Reminders for meals and water intake throughout the day<br>- Simple analytics showing weekly calorie intake and hydration trends<br>- Data stored using InsForge database with strict user-specific access control<br><br>Authentication:<br>- User registration using email and verification code (OTP)<br>- After successful OTP verification, automatically log the user in and create a valid auth session<br>- Authentication sessions must persist correctly across page refreshes and devices<br>- Signing out should fully clear the active session without affecting future logins<br>- Users must be able to sign in again normally after signing out without any auth conflicts</pre><p>That’s it. No tech stack instructions, no database schema, no API design. Just a description of what the app should do and how auth should behave. InsForge and the agent figure out the rest.</p><h4>What the Agent Actually Does</h4><p>After submitting the prompt, the agent doesn’t immediately start writing code. The first thing it does is fetch InsForge’s documentation through the MCP tools. It reads through the available capabilities, understands the tool interfaces, and then it makes a plan.</p><p>You can literally watch it think. It outlines what it needs to build, in what order, with what dependencies. Auth first, then database schema, then the pages that depend on those. It’s not just blindly generating code, it’s reasoning about the build sequence.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*voheyGEGZqeeQ-_D0ajYUQ.jpeg" /></figure><p>Then it starts.</p><p>Files appear. The project structure fills in. Auth utilities, database helpers, the registration flow, the login page, session handling, the dashboard layout, the meal form, the chart components for analytics. You’re watching an app assemble itself.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/322/1*gwTFGvy_pKYSvfWc5PdUgg.png" /></figure><p>When it’s done, it gives you a summary of every table it created, every page it built, what auth configuration is now in place, where the storage bucket was set up. A complete accounting of what exists.</p><h4>Round One: Auth Flow Was Broken</h4><p>First thing I did after the initial build was test the auth flow — registration, OTP verification, login. And it broke immediately.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ANicafX-bjYEC-EuorQtsA.jpeg" /></figure><p>The specific issue: after entering the OTP and verifying the email, users were not being redirected to the dashboard. The verification was completing on InsForge’s side — the user was marked as verified — but the session wasn’t being established correctly on the frontend. So you’d verify, and then just… land back at the login screen. No error, no explanation. Just a blank redirect like nothing happened.</p><p>This is the exact kind of auth edge case that’s infuriating to debug manually. The backend did its job. The problem was in how the frontend was handling the post-verification state — the session token wasn’t being picked up after OTP confirmation.</p><p>I sent this prompt:</p><pre>Fix the authentication flow for the &quot;Fittrack&quot; app built using InsForge. The current issue is that after email OTP verification, users are not redirected to the dashboard and sessions are not being established correctly.</pre><p>The agent traced through the OTP verification callback, found where the session initialization was being dropped, corrected the order of operations, and added proper handling for the post-verification redirect. One prompt, problem fixed.</p><p>I tested again — registered, received the OTP, entered it, and this time landed directly on the dashboard. Session persisting across refresh too. Auth was solid.</p><h4>Round Two: UI Fix</h4><p>With auth working, I took a proper look at the interface and it needed work. The structure was all there — dashboard, meal log, hydration tracker, analytics — but the visual design was rough. Default styling, inconsistent spacing, the kind of thing that tells you immediately an AI built it without any design direction.</p><p>I prompted the agent to redesign the UI. It came back with a few questions first — color scheme, overall vibe, any preferences on layout. I told it: light mode, green accent color, clean and minimal, prioritize readability.</p><p>It went through every page and applied the design system consistently. Navigation, cards, forms, buttons, typography — all of it updated. The before and after was significant. Not perfect, but something I’d actually show someone without being embarrassed.</p><h4>Round Three: Final Polish and Meal Photo Uploads</h4><p>The prompt I sent:</p><pre>Fix meal form reset null error, fetch and show user profile in navbar/profile, maintain auth state, add optional meal photo upload using InsForge storage.</pre><p>A closer walkthrough after the redesign turned up a few more things — minor inconsistencies between pages, a form not resetting correctly after submission, a couple of small UI issues the redesign pass had missed.</p><p>More importantly, I realized I’d left meal photo uploads out of the original prompt entirely. That was a feature I actually wanted — being able to attach a photo to a meal entry.</p><p>I bundled everything into one prompt: the UI fixes, the form issues, and the photo upload feature.</p><p>The photo upload part is where InsForge’s integrated approach really shows its value. The agent didn’t reach for S3 or any third-party storage. It used InsForge Cloud Storage directly. It created a storage bucket, wrote the upload logic, updated the meal form with a file input, handled the optional case correctly, and stored the image URL in the database alongside the meal data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*20ANWR91nN2PiTjiPGUL1A.jpeg" /></figure><h3>The App That Exists Now</h3><p>After those three passes, here’s what FitTrack actually does:</p><p><strong>Auth that works correctly</strong> — Users register with email, receive a verification email, verify and get logged in. Sessions persist across browser refreshes. Logout works from any page and properly clears the session. Edge cases like “what if someone tries to log in before verifying” are handled with a clear error message.</p><p><strong>A useful dashboard</strong> — The main screen shows today at a glance: meals logged, total calories, hydration intake for the day. Quick-add buttons that open the meal and hydration forms without leaving the page. Clean enough that you’d actually want to open it.</p><p><strong>Meal logging with photos</strong> — Add a meal with a name, calorie count, optional description, and optional photo. If you attach a photo, it uploads to InsForge’s cloud storage and the image shows in the meal log. The form resets cleanly after submission. Validation tells you clearly what’s missing if you try to submit an incomplete entry.</p><p><strong>Hydration tracking</strong> — Log water intake in whatever unit you prefer. The dashboard shows progress toward a daily goal. Small but actually useful for people who are bad at drinking water (me).</p><p><strong>Notes</strong> — A simple text field for daily notes. Not everything fits into structured data. Sometimes you just want to write “had a headache all day, probably didn’t drink enough water.” The notes section handles that.</p><p><strong>History and analytics</strong> — Browse your log by date. The analytics page has actual charts — weekly calorie trends, hydration averages, meal frequency. Built with real data from the Postgres database, updating as you add entries.</p><p><strong>Profile management</strong> — Update your display name and basic details. Not complicated, but necessary.</p><p>This is not a prototype. It’s not a demo with hardcoded data. It’s a working application backed by a real Postgres database, with real authentication, and real cloud storage. If I wanted to, I could give this URL to someone and they could actually use it.</p><h3>The InsForge Dashboard: Full Visibility Into What Was Built</h3><p>One thing I want to highlight because it matters for understanding how InsForge works: you can see everything the agent built.</p><p>After the agent finished, I opened the InsForge dashboard and looked at what existed. The database section has multiple tables: users (managed by auth), meals, hydration, and notes. Each table had the right schema — meal records had a user_id foreign key, a name field, a calorie field, a description field, and a photo_url field for the storage reference. Properly structured, properly related.</p><p>The authentication section showed my test user account, verified status, creation timestamp. The storage section showed the bucket created for meal photos and the files that had been uploaded during testing.</p><p>The logs section showed every database operation that had occurred — inserts, reads, the whole history. If something had gone wrong, this is where I’d diagnose it.</p><p>And the deployment section — this is the final step. Navigate there, give the agent a prompt to deploy, and InsForge handles the build process and hosting. You get a live URL. That’s the whole process.</p><h3>Honest Assessment: What This Workflow Is and Isn’t</h3><p>I want to be clear about something. This is not a workflow where you write one prompt and a perfect app appears. That doesn’t exist. What this is:</p><p><strong>It is</strong> a workflow where the feedback loop is so compressed that what would have taken a week of evenings now takes an afternoon.</p><p><strong>It is</strong> a workflow where you’re thinking about features and user experience instead of connection strings and migration files.</p><p><strong>It is</strong> a workflow where when things break — and things will break — fixing them is fast because you’re writing prompts, not debugging infrastructure.</p><p><strong>It isn’t</strong> magic. The app needed three rounds of iteration before I was happy with it. Auth was broken in round one. The UI needed multiple passes. A feature I forgot in the original prompt required a separate pass to add.</p><p><strong>It isn’t</strong> suitable for complex architectural decisions that need careful human judgment. If you’re building something with unusual performance requirements, complex data relationships, or tricky consistency guarantees — you’ll still need to think carefully about those things. InsForge handles the infrastructure; it doesn’t replace architectural thinking.</p><p>What it is, fundamentally, is a dramatic reduction in the overhead between having an idea and having a working version of that idea. The plumbing is handled. You focus on the building.</p><h3>Wrap Up</h3><p>We went from zero to a fully working Diet &amp; Hydration Tracker — with auth, a real Postgres database, cloud storage for meal photos, analytics, and deployment — using nothing but prompts. No backend code written by hand. No infrastructure setup. No debugging sessions wondering why the connection string is wrong in production.</p><p><strong>That’s all for now</strong></p><p><strong>Keep Coding✌️✌️</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25fffc9c8d97" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Securing MCP Servers: From Vulnerable to Bulletproof with Scalekit]]></title>
            <link>https://geekpython.medium.com/securing-mcp-servers-from-vulnerable-to-bulletproof-with-scalekit-6522b07187da?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/6522b07187da</guid>
            <category><![CDATA[oauth2]]></category>
            <category><![CDATA[mcp-server]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[mcps]]></category>
            <category><![CDATA[authentication]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Sat, 21 Feb 2026 16:41:21 GMT</pubDate>
            <atom:updated>2026-02-21T16:41:21.477Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6BVgFb-aDMPVt_hTVpQGjA.png" /></figure><p>Picture this: You’ve just built a sleek MCP (Model Context Protocol) server that powers AI agents for multiple clients. Everything works beautifully until you realize that Client A’s AI assistant can access Client B’s sensitive data. Or worse, an unauthorized user creates, reads, and deletes data from any tenant’s workspace without breaking a sweat.</p><p>This isn’t a hypothetical nightmare. It’s the reality of <strong>unsecured multi-tenant MCP servers</strong>.</p><h3>What is a Multi-Tenant MCP Server?</h3><p>A multi-tenant MCP server is like an apartment building for AI tools. Instead of building separate servers for each client (tenant), you create one powerful server that serves multiple organizations simultaneously. Each tenant gets their own isolated workspace, data, and resources, at least, that’s how it <em>should</em> work.</p><p>The Model Context Protocol (MCP) has revolutionized how AI agents interact with external tools and data sources. But with great power comes great responsibility, especially when you’re serving multiple clients from a single instance.</p><h3>The Multi-Tenant Security Minefield</h3><p>Multi-tenant architectures introduce unique security challenges that can turn your innovative AI solution into a liability. But when <strong>AI agents</strong> become your clients instead of humans, the threat landscape changes fundamentally.</p><h3>Why AI Agents Change Everything</h3><p>Traditional multi-tenant applications serve human users through web browsers or mobile apps. Humans make mistakes, but they’re generally predictable. AI agents, however, operate differently:</p><p><strong>Humans:</strong></p><ul><li>Navigate through UIs with guardrails</li><li>Limited by typing speed and attention span</li><li>Social engineering requires conversation and time</li><li>Can be detected by behavioral anomalies</li></ul><p><strong>AI Agents:</strong></p><ul><li>Make API calls programmatically at machine speed</li><li>Can enumerate thousands of tenant IDs in seconds</li><li>Execute complex multi-step attacks automatically</li><li>Adapt strategies based on responses</li></ul><p><strong>The Critical Difference:</strong> An AI agent acting as an MCP client can systematically probe your server’s security boundaries far more efficiently than any human attacker. Without proper isolation, a compromised or malicious AI agent doesn’t just leak one tenant’s data — it can <strong>exfiltrate entire databases</strong> before you notice.</p><h3>The Cross-Tenant Threat Model: Deep Dive</h3><p>Let’s analyze each threat vector in the context of AI-powered MCP clients:</p><h4>1. Cross-Tenant Data Leakage</h4><p><strong>Traditional Risk:</strong> User A accidentally sees User B’s data through a UI bug.</p><p><strong>AI Agent Risk:</strong> An AI agent systematically queries all possible tenant identifiers, building a complete map of your multi-tenant architecture.</p><p><strong>Why It Matters:</strong> AI agents don’t get tired. They don’t make random typos. They systematically exploit every weakness until they find what works. Without authentication, your entire multi-tenant database is one enumeration attack away from exposure.</p><h4>2. Unauthorized Access at Machine Speed</h4><p><strong>Traditional Risk:</strong> An unauthorized user manually creates a few records.</p><p><strong>AI Agent Risk:</strong> A malicious agent creates thousands of poisoned records across multiple tenants simultaneously, corrupting your entire dataset.</p><p><strong>Why It Matters:</strong> AI agents can inject malicious data at scale. Without authentication, you can’t even trace which agent caused the damage. Was it a bug? A malicious actor? You’ll never know.</p><h4>3. Scope Creep and Permission Escalation</h4><p><strong>Traditional Risk:</strong> A user with read access somehow gets write permissions through a UI bug.</p><p><strong>AI Agent Risk:</strong> An agent probes every endpoint systematically, discovering which operations lack permission checks, then exploits them to gain unauthorized capabilities.</p><p><strong>Why It Matters:</strong> An AI agent will find every permission hole in your API surface within minutes of first connection. It doesn’t need to understand your business logic — it just tries everything.</p><h4>4. Compliance Violations at Scale</h4><p><strong>Traditional Risk:</strong> One user’s data is mishandled, triggering a GDPR complaint.</p><p><strong>AI Agent Risk:</strong> An automated agent accesses data across all tenants, triggering simultaneous violations for every organization you serve.</p><p><strong>Why It Matters:</strong> With human attackers, breaches are usually isolated. With AI agents in a multi-tenant system, <strong>every security failure affects every tenant</strong>. Your liability multiplies by your customer count.</p><h4>5. The Identity Crisis: Who Did What?</h4><p><strong>Traditional Risk:</strong> Can’t track which user performed an action.</p><p><strong>AI Agent Risk:</strong> Can’t even identify which AI agent from which tenant made a request. Complete loss of attribution.</p><p><strong>Why It Matters:</strong></p><ul><li><strong>No audit trail</strong> for compliance audits</li><li><strong>No incident response</strong> — can’t identify attack source</li><li><strong>No rate limiting</strong> per tenant — one agent can DoS everyone</li><li><strong>No accountability</strong> — agents act with impunity</li></ul><h3>The Insecure Foundation</h3><p>To illustrate these risks, let’s examine an unsecured MCP server implementation (don’t worry, we’ll fix it soon):</p><pre># ❌ INSECURE: No authentication, no tenant isolation<br>@mcp.tool<br>def create_todo(title: str, tenant_id: str, description: Optional[str] = None):<br>    # Anyone can pass ANY tenant_id - zero verification!<br>    tenant_store = TODO_STORE.setdefault(tenant_id, {})<br>    todo = TodoItem(id=str(uuid.uuid4()), tenant_id=tenant_id, title=title)<br>    tenant_store[todo.id] = todo<br>    return {&quot;todo&quot;: todo.to_dict()}</pre><p><strong>What’s wrong here?</strong></p><ul><li><strong>No authentication</strong>: Anyone can call this endpoint</li><li><strong>Client-controlled tenant_id</strong>: The user provides their own tenant ID. They could claim to be anyone!</li><li><strong>No scope validation</strong>: Even if we added auth, there’s no check for write permissions</li><li><strong>Zero accountability</strong>: No audit trail of who did what</li></ul><h3>Our Mission: From Vulnerable to Unbreakable</h3><p>In this blog post, we’ll transform that vulnerable server into a secured one. Using <strong>Scalekit’s scoped authentication</strong> and robust MCP security patterns, we’ll demonstrate how to:</p><p>✅ <strong>Implement proper authentication</strong> so only authorized users access your server<br>✅ <strong>Enforce true tenant isolation</strong> where tenant_id comes from verified tokens, not user input<br>✅ <strong>Apply granular permission scopes</strong> (read vs. write) for fine-grained access control<br>✅ <strong>Eliminate cross-tenant threats</strong> completely<br>✅ <strong>Build a production-ready, secure multi-tenant MCP server</strong> you can deploy with confidence</p><p>By the end of this journey, you’ll understand not just <em>how</em> to secure your MCP server, but <em>why</em> each security layer matters. You’ll see the exact code transformation from vulnerable to secure, and you’ll have a blueprint you can apply to your own MCP implementations.</p><h3>Part 1: Building the Vulnerable Server</h3><p>Before we build the secured server, let’s first understand the vulnerabilities by building an intentionally insecure MCP server. This hands-on approach will make the security improvements crystal clear.</p><h3>Setting Up the Development Environment</h3><p>Let’s start by creating a clean Python environment for our MCP server.</p><h4>Step 1: Create a Virtual Environment</h4><p>First, create a new directory for your project and set up a virtual environment:</p><pre># Create project directory<br>mkdir insecure-mcp-server<br>cd insecure-mcp-server<br><br># Create virtual environment<br>python3 -m venv todovenv</pre><p>Activate the environment, and you should see (todovenv) appear in your terminal prompt, indicating the virtual environment is active.</p><h4>Step 2: Install FastMCP</h4><p>FastMCP is a Python framework that makes building MCP servers simple and intuitive. Let’s install it:</p><pre>pip install fastmcp</pre><p>This will install FastMCP along with all its dependencies. You should see output confirming the installation.</p><h4>Step 3: Install Additional Dependencies</h4><p>We’ll need a few more packages for our server:</p><pre>pip install python-dotenv</pre><p>The python-dotenv package allows us to manage environment variables easily (though we won&#39;t use them much in the insecure version).</p><h3>The Insecure Server: A Line-by-Line Breakdown</h3><p>Now, let’s create our vulnerable MCP server. Create a file called server.py and let&#39;s walk through each section:</p><pre>import os<br>import uuid<br>from dataclasses import dataclass, asdict<br>from typing import Optional, Dict<br><br>from dotenv import load_dotenv<br>from fastmcp import FastMCP<br><br>load_dotenv()</pre><p><strong>What’s happening here?</strong></p><ul><li>We import the necessary libraries for our server</li><li>uuid will generate unique IDs for our todo items</li><li>dataclasses provides a clean way to define our data models</li><li>FastMCP is the core framework for building our MCP server</li><li>load_dotenv() loads environment variables (not critical for this insecure version)</li></ul><pre># -------------------------------------------------<br># MCP Server (INSECURE ❌)<br># -------------------------------------------------<br><br>mcp = FastMCP(<br>    &quot;Multi-Tenant Todo MCP&quot;,<br>    stateless_http=True,<br>)</pre><p><strong>🚨 SECURITY FLAW #1: No Authentication</strong></p><p>Notice what’s <strong>missing</strong> here — there’s no auth parameter! This means:</p><ul><li>Anyone can connect to this server</li><li>No verification of who’s making requests</li><li>No way to identify which tenant is accessing data</li><li>Zero accountability or audit trail</li></ul><pre># -------------------------------------------------<br># Data Model<br># -------------------------------------------------<br><br>@dataclass<br>class TodoItem:<br>    id: str<br>    tenant_id: str<br>    title: str<br>    description: Optional[str]<br>    completed: bool = False<br><br>    def to_dict(self):<br>        return asdict(self)</pre><p><strong>Data Model Breakdown:</strong></p><p>Our TodoItem is a simple dataclass that represents a single todo entry:</p><ul><li>id: Unique identifier for each todo</li><li>tenant_id: Which tenant owns this todo (this becomes crucial for isolation)</li><li>title: The todo&#39;s main text</li><li>description: Optional additional details</li><li>completed: Status flag (defaults to False)</li><li>to_dict(): Converts the dataclass to a dictionary for JSON responses</li></ul><p>The model itself is fine — it’s how we use it that creates vulnerabilities.</p><pre># -------------------------------------------------<br># In-Memory Store<br># tenant_id -&gt; { todo_id -&gt; TodoItem }<br># -------------------------------------------------<br><br>TODO_STORE: Dict[str, Dict[str, TodoItem]] = {}</pre><p><strong>Storage Architecture:</strong></p><p>This is a nested dictionary structure:</p><ul><li><strong>Outer dictionary</strong>: Keys are tenant_id strings, representing different tenants</li><li><strong>Inner dictionary</strong>: Keys are todo_id strings, values are TodoItem objects</li></ul><p>Structure visualization:</p><pre>TODO_STORE = {<br>    &quot;tenant_abc&quot;: {<br>        &quot;todo_001&quot;: TodoItem(...),<br>        &quot;todo_002&quot;: TodoItem(...),<br>    },<br>    &quot;tenant_xyz&quot;: {<br>        &quot;todo_003&quot;: TodoItem(...),<br>    }<br>}</pre><p><strong>Why in-memory?</strong> For this demo, we’re keeping it simple. In production, you’d use a real database (PostgreSQL, MongoDB, etc.), but the security principles remain the same.</p><h3>The Vulnerable Tools: Where Things Go Wrong</h3><p>Now let’s look at the actual MCP tools — this is where the security holes become glaringly obvious.</p><pre>@mcp.tool<br>def create_todo(<br>    title: str,<br>    tenant_id: str,<br>    description: Optional[str] = None,<br>) -&gt; dict:<br>    tenant_store = TODO_STORE.setdefault(tenant_id, {})<br>    <br>    todo = TodoItem(<br>        id=str(uuid.uuid4()),<br>        tenant_id=tenant_id,<br>        title=title,<br>        description=description,<br>    )<br>    <br>    tenant_store[todo.id] = todo<br>    return {&quot;todo&quot;: todo.to_dict()}</pre><p><strong>🚨 SECURITY FLAW #2: Client-Controlled Tenant ID</strong></p><p>This is the <strong>worst</strong> security mistake you can make in a multi-tenant system:</p><ol><li><strong>The user provides</strong> tenant_id as a parameter - They can claim to be any tenant</li><li><strong>No verification</strong> — We blindly trust whatever tenant_id they send</li><li><strong>Direct data access</strong> — We immediately use that tenant_id to access the store</li></ol><p><strong>Attack scenario:</strong></p><pre># Attacker discovers tenant IDs through enumeration or social engineering<br>create_todo(title=&quot;Malicious todo&quot;, tenant_id=&quot;victim_company_123&quot;)<br># Boom! They just injected data into another tenant&#39;s workspace</pre><p><strong>What we should do instead:</strong> Extract tenant_id from a verified authentication token, never from user input.</p><pre>@mcp.tool<br>def list_todos(tenant_id: str) -&gt; dict:<br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    return {<br>        &quot;todos&quot;: [todo.to_dict() for todo in tenant_store.values()]<br>    }</pre><p><strong>🚨 SECURITY FLAW #3: Unrestricted Data Access</strong></p><p>Same problem, different tool:</p><ul><li>User controls which tenant’s data to retrieve</li><li>No authentication means anyone can call this</li><li>No authorization means anyone can read any tenant’s data</li></ul><p><strong>Attack scenario:</strong></p><pre># Attacker iterates through possible tenant IDs<br>for tenant_id in [&quot;company_a&quot;, &quot;company_b&quot;, &quot;company_c&quot;]:<br>    todos = list_todos(tenant_id=tenant_id)<br>    # Exfiltrate all todo data from all tenants!</pre><pre>@mcp.tool<br>def get_todo(tenant_id: str, todo_id: str) -&gt; dict:<br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    todo = tenant_store.get(todo_id)<br>    <br>    if not todo:<br>        return {&quot;error&quot;: &quot;Todo not found&quot;}<br>    <br>    return {&quot;todo&quot;: todo.to_dict()}</pre><p><strong>🚨 SECURITY FLAW #4: No Access Control</strong></p><p>Even though we check if the todo exists, we still allow anyone to:</p><ul><li>Probe for valid todo IDs</li><li>Read any todo from any tenant</li></ul><pre>@mcp.tool<br>def delete_todo(tenant_id: str, todo_id: str) -&gt; dict:<br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    <br>    if todo_id not in tenant_store:<br>        return {&quot;error&quot;: &quot;Todo not found&quot;}<br>    <br>    del tenant_store[todo_id]<br>    return {&quot;deleted&quot;: todo_id}</pre><p><strong>🚨 SECURITY FLAW #5: Destructive Operations Without Verification</strong></p><p>Deletion is permanent and destructive, yet:</p><ul><li>No authentication required</li><li>No authorization check</li><li>No confirmation mechanism</li><li>No audit trail</li></ul><p><strong>Attack scenario:</strong></p><pre># Attacker deletes all todos from a competitor<br>delete_todo(tenant_id=&quot;competitor_company&quot;, todo_id=&quot;critical_project_todo&quot;)<br># Data is gone forever (in this in-memory example)</pre><pre># -------------------------------------------------<br># Run Server<br># -------------------------------------------------<br><br>if __name__ == &quot;__main__&quot;:<br>    mcp.run(transport=&quot;http&quot;, port=3002)</pre><p><strong>Server Startup:</strong></p><p>This launches the MCP server on port 3002 using HTTP transport:</p><ul><li>transport=&quot;http&quot; makes it accessible via HTTP requests</li><li>port=3002 is the local port the server listens on</li></ul><h3>Testing the Insecure Server</h3><p>Let’s run this vulnerable server and see it in action:</p><pre>python server.py</pre><p>You should see output indicating the server is running on http://localhost:3002. Actually, you’ll see the server hosting on http://localhost:3002/mcp, this is the default feature of FastMCP.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TimksAhhNrwLGKXM.png" /></figure><p>Now, let’s use the <strong>MCP Inspector</strong> — a powerful tool for testing and debugging MCP servers. Open a <strong>new terminal window</strong> (keep the server running in the first one) and run:</p><pre>npx @modelcontextprotocol/inspector@latest</pre><p>This will launch the MCP Inspector interface in your browser. The Inspector provides a user-friendly GUI to interact with your MCP server.</p><h4>Connecting to Our Insecure Server</h4><p>Once the Inspector opens:</p><ol><li><strong>Connect to the server</strong> by entering the server URL: http://localhost:3002/mcp</li><li>Click on <strong>“Tools”</strong></li><li>Click <strong>“List Tools”</strong> to see all available MCP tools</li></ol><p>You’ll see all our tools displayed:</p><ul><li>create_todo</li><li>list_todos</li><li>get_todo</li><li>delete_todo</li></ul><p><strong>Notice something alarming?</strong> There’s no login screen, no authentication prompt, nothing. You’re directly connected to the server with full access.</p><h4>Demonstrating the Vulnerability: Creating a Todo</h4><p>Let’s create a todo as a legitimate user:</p><ol><li>Click on the create_todo tool</li><li>Fill in the parameters:</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IOMZfG7FOYoU1UIs.png" /></figure><p>3. Click <strong>“Run Tool”</strong></p><p><strong>Response received:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/533/0*OxtaJibnMqS_IsR1.png" /></figure><p>Great! The todo was created successfully. Everything seems to work fine… until we demonstrate the cross-tenant threat.</p><h4>The Cross-Tenant Attack: A Nightmare Scenario</h4><p>Now, imagine a different scenario. Either:</p><ul><li><strong>Maliciously</strong>: An attacker discovers or guesses the tenant_id</li><li><strong>Accidentally</strong>: Another user coincidentally uses the same tenant_id</li><li><strong>Through social engineering</strong>: Someone tricks a user into revealing their tenant ID</li></ul><p>Let’s simulate this attack:</p><ol><li>Click on the list_todos tool</li><li>Enter the <strong>same tenant_id</strong> we used before:</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Y_hcWGStkdCINt4Z.png" /></figure><p>3. Click <strong>“Run Tool”</strong></p><p><strong>Response received:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/536/0*7xlTu9RKUASlF1Dt.png" /></figure><p>What just happened?</p><ul><li>❌ <strong>No authentication required</strong> — Anyone could make this request</li><li>❌ <strong>No verification of identity</strong> — The server has no idea who is asking</li><li>❌ <strong>Complete data exposure</strong> — The entire todo list is now visible to an unauthorized party</li><li>❌ <strong>Confidential information leaked</strong></li></ul><h4>The Attack Chain Continues…</h4><p>An attacker can now:</p><p><strong>1. Read all todos:</strong></p><pre>// list_todos with tenant_id: &quot;SaP Tech&quot;<br>// Result: Full visibility into company&#39;s internal tasks</pre><p><strong>2. Create malicious todos:</strong></p><pre>{<br>  &quot;title&quot;: &quot;Cancel all vendor contracts&quot;,<br>  &quot;tenant_id&quot;: &quot;SaP Tech&quot;,<br>  &quot;description&quot;: &quot;CEO approved - urgent&quot;<br>}<br>// Injecting false information into another tenant&#39;s workspace</pre><p><strong>3. Delete critical tasks:</strong></p><pre>{<br>  &quot;tenant_id&quot;: &quot;SaP Tech&quot;,<br>  &quot;todo_id&quot;: &quot;a1b2c3d4-e5f6-7890-abcd-ef1234567890&quot;<br>}<br>// Permanently destroying important business data</pre><h3>The Real-World Impact</h3><p>This isn’t just a theoretical exercise. In a production environment with this insecure setup:</p><p><strong>Technical Consequences:</strong></p><ul><li>🎯 <strong>Zero tenant isolation</strong>: Every tenant’s data is accessible to everyone</li><li>🚫 <strong>No audit trail</strong>: Can’t even track who did what</li><li>🔓 <strong>Unlimited scope</strong>: Even a “read-only” user could delete everything</li><li>🌐 <strong>Public exposure</strong>: Anyone on the network can access the server</li></ul><h3>Why Traditional Solutions Fall Short</h3><p>You might be thinking: <em>“Can’t I just add a simple API key?”</em></p><p><strong>That’s not enough for multi-tenant systems:</strong></p><pre># ❌ STILL VULNERABLE: Simple API key check<br>API_KEY = &quot;secret_api_key&quot;<br><br>@mcp.tool<br>def create_todo(api_key: str, tenant_id: str, title: str):<br>    if api_key != API_KEY:<br>        return {&quot;error&quot;: &quot;Unauthorized&quot;}<br>    # Problem: tenant_id is STILL user-controlled!<br>    # One API key = access to ALL tenants</pre><p><strong>What we need:</strong></p><p>✅ Authentication that <strong>identifies the user</strong><br>✅ Authorization that <strong>verifies the tenant</strong><br>✅ Scope validation that <strong>limits permissions</strong><br>✅ Token-based tenant extraction (never trust user input)<br>✅ Granular access control (read vs. write vs. delete)</p><p>This is where <strong>Scalekit’s scoped authentication</strong> becomes essential.</p><h3>Part 2: Securing with Scalekit</h3><p>Now comes the transformation. We’re going to take that vulnerable server and turn it into a production-grade, secure multi-tenant MCP server using <strong>Scalekit’s authentication and scoped authorization</strong>.</p><h3>What is Scalekit?</h3><p><a href="https://scalekit.com">Scalekit</a> is an enterprise-ready authentication platform that specializes in <strong>multi-tenant B2B applications</strong>. It provides:</p><ul><li><strong>Enterprise SSO</strong> — SAML, OIDC support for seamless login</li><li><strong>Multi-tenancy built-in</strong> — Tenant isolation is core to its design</li><li><strong>Scoped permissions</strong> — Granular control over what users can do</li><li><strong>Token-based auth</strong> — Secure JWT tokens with embedded claims</li><li><strong>Admin portal</strong> — Easy management of organizations and users</li></ul><p>For MCP servers, Scalekit solves our critical security problems:</p><ol><li><strong>Authentication</strong> — Verifying who is making requests</li><li><strong>Tenant Isolation</strong> — Ensuring users only access their data</li><li><strong>Authorization</strong> — Controlling what users can do (read vs. write)</li></ol><h3>The Secure Server: Code Transformation</h3><p>Create a new file server_secured.py (or update your existing server.py). Let&#39;s walk through the critical changes:</p><h4>1. New Imports: Scalekit Authentication</h4><pre>from fastmcp.server.auth.providers.scalekit import ScalekitProvider<br>from fastmcp.server.dependencies import AccessToken, get_access_token</pre><p><strong>What’s new?</strong></p><ul><li>ScalekitProvider: The authentication provider that integrates Scalekit with FastMCP</li><li>AccessToken: Type definition for the authenticated token object</li><li>get_access_token(): Dependency injection to retrieve the current user&#39;s token</li></ul><p>These imports are the foundation of our security layer.</p><h4>2. Environment Variables for Configuration</h4><pre>load_dotenv()</pre><p>Before initializing the server, we load environment variables. Create a .env file in your project root:</p><pre># .env<br>SCALEKIT_ENVIRONMENT_URL=https://{your-workspace}.scalekit.{dev/com}<br>SCALEKIT_RESOURCE_ID=res_your_resource_id_here<br>SCALEKIT_BASE_URL=http://localhost:3002/</pre><p><strong>Why environment variables?</strong></p><ul><li>🔒 <strong>Security</strong>: Never hardcode credentials in your source code</li><li>🔄 <strong>Flexibility</strong>: Easy to change between dev/staging/production</li><li>✅ <strong>Best practice</strong>: Industry standard for configuration management</li></ul><h4>3. Secured MCP Server Initialization</h4><pre>mcp = FastMCP(<br>    &quot;Multi-Tenant Todo MCP (SECURED)&quot;,<br>    stateless_http=True,<br>    auth=ScalekitProvider(<br>        environment_url=os.getenv(&quot;SCALEKIT_ENVIRONMENT_URL&quot;),<br>        resource_id=os.getenv(&quot;SCALEKIT_RESOURCE_ID&quot;),<br>        base_url=os.getenv(&quot;SCALEKIT_BASE_URL&quot;),<br>    ),<br>)</pre><p><strong>🔐 SECURITY TRANSFORMATION #1: Authentication Layer Added</strong></p><p>This is the <strong>single most important change</strong>. Let’s break down each parameter:</p><p>environment_url - Your Scalekit environment</p><ul><li>To obtain environment url, go to <strong>Scalekit Dashboard</strong> → <strong>Settings</strong> → <strong>Copy the Environment URL</strong></li><li>This is your unique Scalekit instance</li><li>All authentication requests go through this URL</li></ul><p>resource_id - The protected resource</p><ul><li>Format: res_xxxxxxxxxxxxx</li><li>Used for scope validation</li></ul><p>base_url - Your MCP server&#39;s URL</p><ul><li>Where your server is running (e.g., http://localhost:3002/)</li></ul><p><strong>What happens now?</strong></p><ul><li>✅ <strong>Every request is authenticated</strong> — No more anonymous access</li><li>✅ <strong>Valid JWT tokens required</strong> — Scalekit issues and validates tokens</li><li>✅ <strong>Automatic tenant extraction</strong> — Tenant ID comes from the token, not user input</li><li>✅ <strong>Scope enforcement</strong> — Users can only perform allowed actions</li></ul><h4>4. Scope Validation Helper Function</h4><pre>def _require_scope(scope: str) -&gt; Optional[str]:<br>    token: AccessToken = get_access_token()<br>    if scope not in token.scopes:<br>        return f&quot;Insufficient permissions: `{scope}` scope required.&quot;<br>    return None</pre><p><strong>🎯 SECURITY TRANSFORMATION #2: Granular Permissions</strong></p><p>This helper function enforces <strong>scoped authorization</strong>:</p><p><strong>How it works:</strong></p><ol><li>Retrieves the current user’s access token using get_access_token()</li><li>Checks if the required scope exists in token.scopes</li><li>Returns an error message if the scope is missing</li><li>Returns None if the scope is present (permission granted)</li></ol><p><strong>Why this matters:</strong></p><ul><li>📖 <strong>Read-only users</strong> can’t accidentally delete data</li><li>✏️ <strong>Write permissions</strong> are explicitly required for modifications</li><li>🔒 <strong>Principle of least privilege</strong> — Users only get necessary permissions</li><li>🛡️ <strong>Defense in depth</strong> — Multiple layers of security checks</li></ul><h4>5. Secured Tool: create_todo</h4><pre>@mcp.tool<br>def create_todo(<br>    title: str,<br>    description: Optional[str] = None,<br>) -&gt; dict:<br>    <br>    error = _require_scope(&quot;todo:write&quot;)<br>    if error:<br>        return {&quot;error&quot;: error}<br><br>    <br>    token: AccessToken = get_access_token()<br>    tenant_id = token.claims.get(&quot;oid&quot;)<br><br>    if not tenant_id:<br>        return {&quot;error&quot;: &quot;tenant_id missing in access token&quot;}<br><br>    tenant_store = TODO_STORE.setdefault(tenant_id, {})<br><br>    todo = TodoItem(<br>        id=str(uuid.uuid4()),<br>        tenant_id=tenant_id,<br>        title=title,<br>        description=description,<br>    )<br><br>    tenant_store[todo.id] = todo<br>    return {&quot;todo&quot;: todo.to_dict()}</pre><p><strong>🛡️ CRITICAL SECURITY IMPROVEMENTS:</strong></p><p><strong>1. No</strong> tenant_id parameter!</p><pre># ❌ BEFORE (Insecure):<br>def create_todo(title: str, tenant_id: str, ...):<br><br># ✅ AFTER (Secure):<br>def create_todo(title: str, description: Optional[str] = None):</pre><p><strong>Why?</strong> Users can no longer provide their own tenant_id. This eliminates the entire attack vector!</p><p><strong>2. Scope validation first:</strong></p><pre>error = _require_scope(&quot;todo:write&quot;)<br>if error:<br>    return {&quot;error&quot;: error}</pre><p><strong>Result:</strong> Only users with todo:write scope can create todos. Read-only users are blocked immediately.</p><p><strong>3. Tenant extraction from token:</strong></p><pre>token: AccessToken = get_access_token()<br>tenant_id = token.claims.get(&quot;oid&quot;)</pre><p><strong>Result:</strong> The oid comes from Scalekit&#39;s <strong>cryptographically signed JWT token</strong>, not user input. Tampering is impossible.</p><p><strong>Attack prevention:</strong></p><ul><li>❌ Can’t specify a different tenant_id</li><li>❌ Can’t bypass authentication</li><li>❌ Can’t escalate privileges (no write scope = no creation)</li><li>✅ Can ONLY create todos in your own tenant</li></ul><h4>6. Secured Tool: list_todos</h4><pre>@mcp.tool<br>def list_todos() -&gt; dict:<br>    <br>    error = _require_scope(&quot;todo:read&quot;)<br>    if error:<br>        return {&quot;error&quot;: error}<br><br>    <br>    token: AccessToken = get_access_token()<br>    tenant_id = token.claims.get(&quot;oid&quot;)<br><br>    if not tenant_id:<br>        return {&quot;error&quot;: &quot;tenant_id missing in access token&quot;}<br><br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    return {<br>        &quot;todos&quot;: [todo.to_dict() for todo in tenant_store.values()]<br>    }</pre><p><strong>🔒 TRANSFORMATION HIGHLIGHTS:</strong></p><p><strong>Before (Insecure):</strong></p><pre>def list_todos(tenant_id: str):  # User controls which tenant!<br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    return {&quot;todos&quot;: [...]}</pre><p><strong>After (Secure):</strong></p><pre>def list_todos():  # NO tenant_id parameter!<br>    error = _require_scope(&quot;todo:read&quot;)  # Must have read permission<br>    tenant_id = token.claims.get(&quot;oid&quot;)  # From verified token<br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    return {&quot;todos&quot;: [...]}</pre><p><strong>Result:</strong> Users can only see their own tenant’s todos. Cross-tenant data access is impossible.</p><h4>7. Secured Tool: get_todo</h4><pre>@mcp.tool<br>def get_todo(todo_id: str) -&gt; dict:<br>    error = _require_scope(&quot;todo:read&quot;)<br>    if error:<br>        return {&quot;error&quot;: error}<br><br>    token: AccessToken = get_access_token()<br>    tenant_id = token.claims.get(&quot;oid&quot;)<br><br>    if not tenant_id:<br>        return {&quot;error&quot;: &quot;tenant_id missing in access token&quot;}<br><br>    tenant_store = TODO_STORE.get(tenant_id, {})<br>    todo = tenant_store.get(todo_id)<br><br>    if not todo:<br>        return {&quot;error&quot;: &quot;Todo not found&quot;}<br><br>    return {&quot;todo&quot;: todo.to_dict()}</pre><p><strong>🎯 SECURITY IMPROVEMENT:</strong></p><p>Even though the user provides todo_id, they can only access todos from <strong>their own tenant</strong>:</p><pre>tenant_store = TODO_STORE.get(tenant_id, {})  # tenant_id from token!<br>todo = tenant_store.get(todo_id)</pre><p><strong>Attack scenario prevented:</strong></p><pre>❌ Attacker tries: get_todo(todo_id=&quot;other_tenant_todo_id&quot;)<br>✅ Result: &quot;Todo not found&quot; (because it&#39;s not in THEIR tenant_store)</pre><p>The todo_id lookup is <strong>scoped to the authenticated tenant</strong> automatically!</p><h4>8. Secured Tool: delete_todo</h4><pre>@mcp.tool<br>def delete_todo(todo_id: str) -&gt; dict:<br>    <br>    error = _require_scope(&quot;todo:write&quot;)<br>    if error:<br>        return {&quot;error&quot;: error}<br><br>    token: AccessToken = get_access_token()<br>    tenant_id = token.claims.get(&quot;oid&quot;)<br><br>    if not tenant_id:<br>        return {&quot;error&quot;: &quot;tenant_id missing in access token&quot;}<br><br>    tenant_store = TODO_STORE.get(tenant_id, {})<br><br>    if todo_id not in tenant_store:<br>        return {&quot;error&quot;: &quot;Todo not found&quot;}<br><br>    del tenant_store[todo_id]<br>    return {&quot;deleted&quot;: todo_id}</pre><p><strong>CRITICAL PROTECTION FOR DESTRUCTIVE OPERATIONS:</strong></p><p>Notice: _require_scope(&quot;todo:write&quot;) - not todo:read!</p><p><strong>Why this matters:</strong></p><ul><li>Read-only users can view todos but <strong>cannot delete</strong> them</li><li>Only users with explicit write permissions can perform destructive actions</li><li>Prevents accidental data loss from over-privileged accounts</li></ul><p><strong>Defense layers:</strong></p><ol><li>✅ Must be authenticated (Scalekit token required)</li><li>✅ Must have todo:write scope (not just todo:read)</li><li>✅ Can only delete from their own tenant (token-based tenant_id)</li><li>✅ Todo must exist in their tenant (not someone else’s)</li></ol><p>Now that we understand the code, let’s configure the authentication layer in Scalekit. But first, let’s understand <strong>Why this architecture works</strong> and <strong>How the security actually functions</strong> under the hood.</p><h3>Understanding the Technical Foundation: Why Scalekit Works</h3><p>Before clicking through dashboards, it’s crucial to understand the cryptographic and architectural principles that make this secure.</p><h4>The JWT Token</h4><p>When Scalekit authenticates a user, it issues a <strong>JSON Web Token (JWT)</strong>. This isn’t just a random string — it’s a cryptographically signed proof of identity.</p><p><strong>JWT Structure:</strong></p><p>A JWT has three parts, separated by dots: header.payload.signature</p><pre>eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidGVuYW50X2lkIjoiYWNtZV9jb3JwIiwic2NvcGVzIjpbInRvZG86cmVhZCIsInRvZG86d3JpdGUiXX0.signature_hash</pre><p>Let’s decode each part:</p><p><strong>1. Header (Algorithm + Token Type):</strong></p><pre>{<br>  &quot;alg&quot;: &quot;RS256&quot;,  // RSA signature with SHA-256<br>  &quot;typ&quot;: &quot;JWT&quot;     // Token type<br>}</pre><p><strong>2. Payload (The Claims — This is what matters):</strong></p><pre>{<br>  &quot;user_id&quot;: &quot;user_abc123&quot;,<br>  &quot;oid&quot;: &quot;xyz_org&quot;,  // ← THE CRITICAL CLAIM<br>  &quot;scopes&quot;: [&quot;todo:read&quot;, &quot;todo:write&quot;],<br>  &quot;iss&quot;: &quot;https://2minutespy.scalekit.dev&quot;,  // Issuer<br>  &quot;sub&quot;: &quot;user_abc123&quot;,  // Subject<br>  &quot;aud&quot;: &quot;res_112158011463566594&quot;,  // Audience (your resource)<br>  &quot;exp&quot;: 1735689600,  // Expiration timestamp<br>  &quot;iat&quot;: 1735603200,  // Issued at<br>  &quot;jti&quot;: &quot;unique-token-id&quot;  // JWT ID<br>}</pre><p><strong>3. Signature (Cryptographic Proof):</strong></p><pre>RSASHA256(<br>  base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload),<br>  scalekit_private_key<br>)</pre><p><strong>Why This Is Secure:</strong></p><p>🔐 <strong>Tamper-Proof:</strong> The signature is created using Scalekit’s <strong>private key</strong>. If you change even one character in the header or payload, the signature becomes invalid. Your MCP server verifies the signature using Scalekit’s <strong>public key</strong>.</p><p>🎯 <strong>oid (similar to tenant id) in the Claims:</strong> Notice &quot;oid&quot;: &quot;xyz_org&quot; in the payload? This is set by Scalekit based on which organization the user belongs to. The user <strong>never provides this</strong> - Scalekit determines it during authentication.</p><p>⚡ <strong>Scopes Embedded:</strong> The scopes array is also in the token. Your code checks token.scopes - these scopes were assigned in Scalekit&#39;s dashboard and are cryptographically bound to this user.</p><p>⏰ <strong>Time-Bounded:</strong> The exp (expiration) claim means tokens automatically expire. Even if somehow compromised, the window of vulnerability is limited.</p><h4>How tenant_id (oid) Ends Up in token.claims</h4><p>Let’s trace exactly how tenant_id flows from Scalekit to your code:</p><p><strong>Step 1: User Authentication</strong></p><pre>User → Scalekit Login → &quot;Who is this user?&quot;<br>Scalekit checks: email → user_abc123 → belongs to organization → xyz_org</pre><p><strong>Step 2: JWT Generation</strong></p><pre>Scalekit creates JWT payload:<br>{<br>  &quot;user_id&quot;: &quot;user_abc123&quot;,<br>  &quot;tenant_id&quot;: &quot;xyz_org&quot;,    Determined by Scalekit, not user input<br>  &quot;scopes&quot;: [&quot;todo:read&quot;, &quot;todo:write&quot;]<br>}<br><br>Signs it with private key → JWT token</pre><p><strong>Step 3: Token Delivery</strong></p><pre>JWT → Browser → MCP Inspector → Stored in Authorization header</pre><p><strong>Step 4: Request to Your MCP Server</strong></p><pre>HTTP Request:<br>POST /create_todo<br>Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...<br>Body: { &quot;title&quot;: &quot;My todo&quot; }</pre><p><strong>Step 5: FastMCP + ScalekitProvider Validation</strong></p><pre># Inside FastMCP&#39;s authentication middleware<br>token_string = request.headers[&quot;Authorization&quot;].replace(&quot;Bearer &quot;, &quot;&quot;)<br><br># ScalekitProvider validates:<br>1. Verify signature using Scalekit&#39;s public key<br>2. Check expiration (exp claim)<br>3. Verify audience (aud) matches your resource_id<br>4. Extract all claims into AccessToken object<br><br>access_token = AccessToken(<br>    user_id=&quot;user_abc123&quot;,<br>    tenant_id=&quot;xyz_org&quot;,  # From JWT claims<br>    scopes=[&quot;todo:read&quot;, &quot;todo:write&quot;],<br>    claims={...}  # Full JWT payload<br>)</pre><p><strong>Step 6: Your Code Accesses It</strong></p><pre>@mcp.tool<br>def create_todo(title: str):<br>    token: AccessToken = get_access_token()  # Injected by FastMCP<br>    tenant_id = token.claims.get(&quot;oid&quot;)  # &quot;xyz_org&quot;<br>    <br>    # THIS tenant_id:<br>    # ✓ Came from a cryptographically signed JWT<br>    # ✓ Was determined by Scalekit based on user&#39;s org<br>    # ✓ Cannot be forged or modified<br>    # ✓ Is absolutely trustworthy</pre><h3>Part 3: Where Do These Scopes and Resource IDs Come From?</h3><p>You might be wondering: <em>“Where did</em> todo:read, todo:write, and resource_id come from?&quot;</p><p>Great question! These aren’t magical — they’re <strong>configured in Scalekit’s dashboard</strong>.</p><h3>Step-by-Step Dashboard Configuration</h3><p>Let’s walk through registering our MCP server in Scalekit:</p><h4>Step 1: Access the Scalekit Dashboard</h4><p>First, head over to the Scalekit website and log in to your account:</p><ol><li>Navigate to <a href="https://scalekit.com"><strong>https://scalekit.com</strong></a></li><li>Click <strong>“Sign In”</strong> and enter your credentials</li></ol><p>Once logged in, you’ll see the main dashboard.</p><h4>Step 2: Add Your MCP Server</h4><p>Now let’s register our MCP server:</p><ol><li>In the sidebar, under Configure, click on <strong>“MCP Server”</strong></li><li>Click the <strong>“Add MCP Server”</strong> button</li></ol><p>You’ll see a form to configure your new MCP server.</p><h4>Step 3: Name Your Server</h4><p>In the form, enter a descriptive name for your server:</p><p><strong>Server Name:</strong></p><pre>ToDo MCP Server</pre><p>This name helps you identify this server in the dashboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/849/0*G5jbxDXcA34BS1-D.png" /></figure><h4>Step 4: Configure Server URL</h4><p>In the <strong>Configuration</strong> section, specify your MCP server’s URL:</p><p><strong>Server URL:</strong></p><pre>http://localhost:3002/</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/791/0*JxvFR8mLZDbiWhsN.png" /></figure><p><strong>Important notes:</strong></p><ul><li>For local development: http://localhost:3002/</li><li>For production: https://api.yourdomain.com/</li><li><strong>Always include the trailing slash</strong> (/)</li><li>Make sure the port matches your MCP server’s port (3002)</li></ul><h4>Step 5: Add Scopes (Advanced Configuration)</h4><p>Click on <strong>“Advanced Configuration”</strong> to expand the scopes section.</p><p>Now add your two scopes:</p><p><strong>Scope 1: Read Permission</strong></p><pre>Scope Name: todo:read<br>Description: To read todo</pre><p>Click <strong>“Add Permission”</strong>.</p><p><strong>Scope 2: Write Permission</strong></p><pre>Scope Name: todo:write<br>Description: To create, update and delete todo</pre><p>Click <strong>“Add Permission”</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/952/1*0Ptvq1ITli981K-3SblN2w.png" /></figure><h4>Step 6: Save All Changes</h4><p>Review your configuration:</p><p>✅ Server Name: ToDo MCP Server<br>✅ Server URL: http://localhost:3002/<br>✅ Scopes: todo:read, todo:write</p><p>Click <strong>“Save”</strong> to finalize the configuration.</p><h4>Step 7: Copy the Resource ID</h4><p>After saving, you’ll see your server in the list. Next to your server name, you’ll see a <strong>Resource ID</strong> displayed:</p><pre>res_11215801146356xxxx</pre><p><strong>📋 Copy this Resource ID</strong> — you’ll need it in your .env file!</p><p>The Resource ID format always starts with res_ followed by numbers. This uniquely identifies your MCP server in Scalekit.</p><h4>Step 8: Update Your .env File</h4><p>Now, add this Resource ID to your environment configuration.</p><p>Open (or create) your .env file in your project root directory:</p><pre># .env file<br># Your Scalekit environment URL<br>SCALEKIT_ENVIRONMENT_URL=https://{your-workspace}.scalekit.{dev/com}<br># The Resource ID you just copied (starts with res_)<br>SCALEKIT_RESOURCE_ID=res_11215801146356xxxx<br># Your MCP server&#39;s base URL<br>SCALEKIT_BASE_URL=http://localhost:3002/</pre><p><strong>Where to find the other values:</strong></p><ul><li><strong>SCALEKIT_ENVIRONMENT_URL</strong>: Found in your Scalekit dashboard settings</li><li><strong>SCALEKIT_BASE_URL</strong>: Your MCP server’s running URL (same as configured in Step 4)</li></ul><h3>Part 4: Testing the Secured Server — Scoped Auth in Action</h3><p>Now comes the exciting part, let’s test our secured MCP server and see how Scalekit’s scoped authentication protects our multi-tenant system.</p><h3>Starting the Server and MCP Inspector</h3><p>First, let’s get both components running:</p><h4>Terminal 1: Start the Secured MCP Server</h4><pre># Make sure you&#39;re in your project directory<br># and virtual environment is activated<br>python server.py</pre><p>You should see output indicating the server is running.</p><p><strong>Important:</strong> Keep this terminal running!</p><h4>Terminal 2: Start the MCP Inspector</h4><p>Open a <strong>new terminal window</strong> and run:</p><pre>npx @modelcontextprotocol/inspector@latest</pre><p>The MCP Inspector will open in your browser.</p><h3>Configuring Scoped Permissions</h3><p>Before we test, let’s configure our user to have <strong>read-only permissions</strong> to demonstrate scope enforcement.</p><p><strong>In your Scalekit Dashboard:</strong></p><ol><li>Navigate to <strong>your registered MCP server</strong></li><li>Edit your MCP server by clicking the three (…) dots</li><li>Go to <strong>Configuration</strong> section and then <strong>Advanced Configuration</strong> section</li><li><strong>Assign only the</strong> todo:read scope</li><li><strong>Do NOT assign</strong> todo:write (we&#39;ll test this restriction)</li><li>Save the changes</li></ol><p>This simulates a <strong>read-only user</strong> — someone who should be able to view todos but not create or delete them.</p><h3>Authenticating with OAuth</h3><p>Now let’s authenticate properly using Scalekit’s OAuth flow.</p><p><strong>In MCP Inspector (center of the screen):</strong></p><ol><li>Look for the <strong>“Open Auth Settings”</strong> button</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1IQqZO-IRxtOrX26.png" /></figure><p>2. Click <strong>“Open Auth Settings”</strong></p><p>3. You’ll see authentication options — click <strong>“Quick OAuth Flow”</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1017/0*45Zdg8mIpZ9nfSVR.png" /></figure><p><strong>Authentication Flow Begins:</strong></p><p>A new browser window/tab will open with the Scalekit login page.</p><p><strong>Steps:</strong></p><ol><li><strong>Enter your email address</strong></li><li>Complete the authentication</li><li><strong>Authorize the MCP Inspector</strong> to access your MCP server</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/786/1*DtqhkXvyucdjHG6yBLpB5Q.png" /></figure><p>4. You’ll be redirected back to the Inspector</p><p><strong>Expected Result: ✅ Connection Successful</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/0*acaGmWH4NFJDlvr-.png" /></figure><p><strong>Key Security Point:</strong> The server now knows <strong>exactly who you are</strong>, <strong>which tenant you belong to</strong>, and <strong>what permissions you have</strong>.</p><h4>The OAuth Flow: End-to-End</h4><p>Let’s visualize the complete OAuth flow that happens when a user authenticates:</p><pre><br>1. User Opens MCP Inspector<br>   └─&gt; Clicks &quot;Connect&quot;<br>       └─&gt; Inspector sees: Authentication Required<br>   <br>2. Inspector → Scalekit Authorization Endpoint<br>   GET https://myorg.scalekit.dev/oauth/authorize?<br>       &amp;resource_id=res_11215801146356xxxx<br>       &amp;redirect_uri=http://localhost:6274/oauth/callback<br>       &amp;response_type=code<br>       &amp;scope=todo:read<br><br>3. User Redirected to Scalekit Login Page<br>   └─&gt; Enters email: user@xyz.com<br>       └─&gt; Scalekit identifies: &quot;This user belongs to xyz_org&quot;<br>   <br>4. User Completes Authentication<br>   └─&gt; Password / SSO / Magic Link<br>       └─&gt; Scalekit verifies identity<br>   <br>5. User Grants Permission<br>   └─&gt; &quot;Allow MCP Inspector to access your todos?&quot;<br>       └─&gt; User clicks &quot;Authorize&quot;<br>   <br>6. Scalekit → Inspector Redirect with Authorization Code<br>   GET http://localhost:6274/oauth/callback?code=AUTHORIZATION_CODE_123<br>   <br>7. Inspector → Scalekit Token Exchange<br>   POST https://myorg.scalekit.dev/oauth/token<br>   Body:<br>   {<br>     &quot;grant_type&quot;: &quot;authorization_code&quot;,<br>     &quot;code&quot;: &quot;AUTHORIZATION_CODE_123&quot;,<br>     &quot;redirect_uri&quot;: &quot;http://localhost:6274/oauth/callback&quot;<br>   }<br>8. Scalekit Generates JWT<br>   └─&gt; Looks up user&#39;s organization: xyz_org<br>   └─&gt; Looks up user&#39;s scopes: [&quot;todo:read&quot;]<br>   └─&gt; Creates JWT payload with tenant_id and scopes<br>   └─&gt; Signs with private key<br>   <br>9. Scalekit → Inspector: Access Token<br>   Response:<br>   {<br>     &quot;access_token&quot;: &quot;eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...&quot;,<br>     &quot;token_type&quot;: &quot;Bearer&quot;,<br>     &quot;expires_in&quot;: 3600,<br>     &quot;scope&quot;: &quot;todo:read&quot;<br>   }<br><br>10. Inspector Stores Token<br>    └─&gt; All future requests include:<br>        Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...<br><br>11. Inspector → MCP Server Request<br>    POST http://localhost:3002/mcp/create_todo<br>    Headers: { &quot;Authorization&quot;: &quot;Bearer eyJh...&quot; }<br>    Body: { &quot;title&quot;: &quot;My todo&quot; }<br><br>12. FastMCP Middleware Intercepts<br>    └─&gt; Extracts JWT from Authorization header<br>    └─&gt; ScalekitProvider validates signature<br>    └─&gt; Checks expiration, audience, issuer<br>    └─&gt; Decodes claims<br>    └─&gt; Creates AccessToken object<br>    <br>13. Your Tool Function Executes<br>    def create_todo(title: str):<br>        token = get_access_token()<br>        tenant_id = token.claims.get(&quot;oid&quot;)  # &quot;xyz_org&quot;<br>        # Operates on correct tenant&#39;s data<br>        <br>14. Response → Inspector → User</pre><h3>Attempting to Create a Todo</h3><p>Now for the critical test — let’s try to create a todo with <strong>read-only permissions</strong>.</p><p><strong>In MCP Inspector:</strong></p><ol><li>Click on <strong>“Tools”</strong> in the navigation</li><li>Click <strong>“List Tools”</strong> to see all available tools</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XIuNpYUS2yt7pT2Y.png" /></figure><p>3. Click on create_todo tool</p><p><strong>Fill in the parameters:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1016/0*MZU11aIwxNw8AV6Q.png" /></figure><p><strong>Notice:</strong> We’re not providing tenant_id. The server will extract it from our token</p><p>4. Click <strong>“Run Tool”</strong></p><p><strong>Expected Result: ❌ Permission Denied</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/478/0*oTs3uEkGaTLC6rKr.png" /></figure><p><strong>🎯 SCOPED AUTH IN ACTION!</strong></p><p><strong>What just happened:</strong></p><pre># Inside create_todo function:<br>error = _require_scope(&quot;todo:write&quot;)  # Checks token.scopes<br>if error:<br>    return {&quot;error&quot;: error}  # Blocked here!</pre><p>The function checked our token and found:</p><ul><li>We are authenticated (valid token)</li><li>We belong to a tenant (tenant_id present)</li><li>We don’t have todo:write in our scopes</li><li><strong>Result:</strong> Access denied before any data operation</li></ul><p><strong>Compare to Insecure Version:</strong></p><pre># ❌ Insecure version:<br>def create_todo(title: str, tenant_id: str):<br>    # No checks! Anyone can create anything!<br>    tenant_store = TODO_STORE.setdefault(tenant_id, {})<br>    todo = TodoItem(...)</pre><p><strong>vs.</strong></p><pre># ✅ Secured version:<br>def create_todo(title: str):<br>    error = _require_scope(&quot;todo:write&quot;)  # Scope check first!<br>    if error:<br>        return {&quot;error&quot;: error}<br>    <br>    token = get_access_token()<br>    tenant_id = token.claims.get(&quot;oid&quot;)  # From verified token</pre><h3>Final Thought</h3><p>Building secure multi-tenant systems doesn’t have to be complicated. With the right tools like <strong>Scalekit</strong> and proper architectural patterns, you can transform a vulnerable prototype into a production-ready, enterprise-grade MCP server in hours, not weeks.</p><p>Whether you’re building MCP servers, B2B SaaS applications, or enterprise tools — Scalekit makes authentication and authorization <strong>simple, secure, and scalable</strong>.</p><p><strong>That’s all for now</strong></p><p><strong>Keep Coding✌✌</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6522b07187da" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Power-up API Testing with EchoAPI’s AI]]></title>
            <link>https://geekpython.medium.com/power-up-api-testing-with-echoapis-ai-ec329c6918a8?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/ec329c6918a8</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[api]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Thu, 25 Sep 2025 16:43:19 GMT</pubDate>
            <atom:updated>2025-09-25T16:43:19.507Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c9Q0me1pvopz0g2xTTqQVw.png" /></figure><p>When you’re building an application, one thing you can’t skip is <strong>API testing</strong>. Whether it’s a login flow, payment gateway, or a complex e-commerce workflow, ensuring your APIs behave correctly saves you from nasty surprises in production.</p><p>I had already explored <strong>EchoAPI</strong> earlier, but while working on my recent project, I revisited it. And guess what? They’ve supercharged it with <strong>AI-powered features</strong>.</p><p>This update caught my attention immediately:</p><ul><li>✅ <strong>One-click Docs</strong></li><li>✅ <strong>Fake data generator</strong></li><li>✅ <strong>Bulk parameter updates</strong></li><li>✅ <strong>AI Convert</strong></li><li>✅ <strong>AI Test-case generator</strong></li><li>✅ <strong>AI-powered script &amp; assertion generator</strong></li></ul><p>In this blog, I’ll take you through my full experience, step by step, so you’ll know exactly how these features can save you <strong>hours of manual testing</strong>.</p><h3>Installing and Setting Up EchoAPI</h3><p>Before we dive into the AI features, let’s start with the basics.</p><p>I downloaded and installed <a href="https://www.echoapi.com/download"><strong>EchoAPI’s desktop app</strong></a>. The interface is clean and very intuitive, something between Postman and Insomnia, but with its own unique <strong>AI touch</strong>.</p><p>For this demo, I connected my <strong>FastAPI e-commerce backend</strong>. This app has typical endpoints like:</p><ul><li>POST /signup</li><li>POST /login</li><li>GET/cart</li><li>POST /cart/add</li><li>POST /order/place</li><li>POST /payment</li><li>GET/products</li><li>GET/products/{product_id}</li></ul><p>Perfect playground to test EchoAPI’s AI magic.</p><h3>AI-Generated Test Cases</h3><p>Let’s start with the <strong>AI Test-case Generator</strong>.</p><p>Normally, writing test cases for each endpoint is a tedious process:</p><ul><li>You have to decide scenarios.</li><li>Write expected outcomes.</li><li>Prepare data for each run.</li><li>And then execute them manually.</li></ul><p>But EchoAPI just makes it <strong>one-click</strong>.</p><h3>Step 1: Pick an Endpoint</h3><p>Inside EchoAPI, I selected an endpoint from my FastAPI project. Let’s say the signup endpoint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*U4-5CLlxan9u6UeZ.png" /></figure><h3>Step 2: Go to the Cases Section</h3><p>Here, EchoAPI shows <strong>8 standard testing dimensions</strong> (like API Specification Compliance Tests, Invalid Input Handling Tests, HTTP Method Validation, etc.). I kept all of them selected.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/926/0*kkI-IyCJ_ZKRVlEg.png" /></figure><h3>Step 3: Generate</h3><p>I clicked <strong>Generate</strong>, and instantly, the AI created:</p><ul><li><strong>Case Objectives</strong> (e.g., <em>“Test signup with valid data”</em>, <em>“Test signup with missing email”</em>)</li><li><strong>Expected Outcomes</strong> (e.g.,<em> “Should return 200 OK”</em>, <em>“Should return 400 Bad Request”</em>)</li><li><strong>Test Data</strong> (realistic values automatically generated)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/0*zTBRb0_BBFHqJpZ7.png" /></figure><h3>Step 4: Apply &amp; Run</h3><p>When I clicked <strong>Apply and Test</strong>, EchoAPI ran <strong>13 cases with 35 different sets of data</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/932/0*WcTSscncISkdJ_XI.png" /></figure><p>That’s a massive workload handled automatically. Imagine writing these manually; it could easily take a couple of hours per endpoint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LO1i2LCFtDymx4be.png" /></figure><p>And here’s the best part: if something fails, EchoAPI shows why it <strong>exactly failed</strong>, with every detail in the response section.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/934/0*DU2l8BhYo1c1tI7I.png" /></figure><p><em>Tip: You can re-run failed cases separately to debug faster.</em></p><h3>AI-Powered Scripts &amp; Assertions</h3><p>Sometimes you need more than automated test cases; you want <strong>custom scripts and validation rules</strong>. EchoAPI covers that too.</p><h3>AI Script Generator</h3><p>Let’s say you want a test script for checking a complex payment flow. Instead of writing it yourself, you just ask the AI:</p><blockquote>I typed “Generate a script to test a payment API with both valid and invalid card details.”</blockquote><p>EchoAPI writes it out, and the script is ready to run.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nlLPsg4aOad-cKVm.png" /></figure><p><em>You can edit these AI-generated scripts or chain them with your own logic if needed.</em></p><h3>AI Assertions</h3><p>Assertions are what validate your API’s response. Doing this manually is slow, especially when responses are large.</p><p>Example:</p><ul><li>You send a request.</li><li>You get a JSON response like the following:</li></ul><pre>{ &quot;status&quot;: &quot;success&quot;, &quot;order_id&quot;: &quot;12345&quot; }</pre><p>Now, instead of manually writing like this:</p><pre>assert response.status == &quot;success&quot;<br>assert &quot;order_id&quot; in response</pre><p>You just click <strong>AI Assert</strong> in the <strong>response section</strong>, and EchoAPI generates these checks for you.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/936/0*t9CJ4uxjZUOc41ej.png" /></figure><p>Even better, you can <strong>add or modify assertions in plain English</strong>:</p><blockquote>“Assert that the response returns status 201 and user_id should not be empty”</blockquote><p>And it updates your tests. This makes it super friendly even for non-coders who just want to validate business logic.</p><h3>Chaining Multiple APIs</h3><p>Real-world apps rarely run on a single endpoint. You usually need a <strong>workflow</strong>: signup → login → add-to-cart → place-order → make-payment.</p><p>EchoAPI lets you <strong>chain APIs together</strong> so you can test them all in one flow.</p><p>Here’s how I did it:</p><h3>Step 1: Create Folder &amp; Subfolder</h3><p>First, go to the <strong>“Tests”</strong> section. Here, I created a folder in EchoAPI for my test flows, then a subfolder for this specific checkout flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/0*Ntgy2rwyeMRFmI30.png" /></figure><h3>Step 2: Add Endpoints by Reference</h3><p>I selected my endpoints into this folder <strong>by reference</strong> (not by copy). This way, if my endpoint changes, it stays automatically synchronized here.</p><h3>Step 3: Configure Each Endpoint</h3><ul><li><strong>Signup Endpoint</strong>: Entered new user data and saved.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/993/0*v9uvEGONW6fPk3Nz.png" /></figure><ul><li><strong>Login Endpoint</strong>: Used the same credentials. This gives me access_token.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/694/0*Na7Fmqn7kRiU4b_n.png" /></figure><ul><li>Captured the <strong>access token</strong> from the response and stored it as a variable using JSONPath expression in the Post-response section.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/994/1*FRZ7jX8ro71WsQofknYpkA.png" /></figure><ul><li><strong>Add-to-Cart Endpoint</strong>: Added a product. Passed the access token in the Auth header.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*O0IyXWqAEmZoJMVs.png" /></figure><ul><li><strong>Place-Order Endpoint</strong>: Placed the order. Passed the access token in the Auth header. Captured the <strong>order ID</strong> and stored it as a variable in the Post-response section.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GoI1jiTDBbOK9Q1J.png" /></figure><ul><li><strong>Make-Payment Endpoint</strong>: Used the captured order ID and passed the access token in Auth header to complete the payment.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TvSwVwcP9JKtiAM7.png" /></figure><h3>Step 4: Run the Flow</h3><p>After choosing the preferred environment, I saved everything and ran the whole chain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/0*I2OFCJBwxn7GQSwv.png" /></figure><p>Result? EchoAPI executed the flow step by step, showing me <strong>where things passed or failed</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/0*ldnZSLpwP0EB1f9I.png" /></figure><p>This is a <strong>game-changer;</strong> instead of manually juggling values between requests, everything flows automatically.</p><h3>EchoAPI vs Postman &amp; Thunder Client: AI Features Comparison</h3><p>When it comes to developer tools, <strong>positioning matters</strong>, not just what features you have, but how they stand out and why someone should choose your solution over existing ones. Let’s break down how EchoAPI compares with Postman and Thunder Client.</p><h3>Postman: AI Features &amp; Strengths</h3><p>Postman has long been the industry leader for API testing and collaboration, and over time, they’ve been adding AI capabilities. Some highlights include:</p><ul><li><strong>Postbot (AI Assistant):</strong> Helps debug APIs, write tests, and analyze large responses or datasets with ease.</li><li><strong>AI Agent Builder:</strong> Lets you build agent-like workflows by combining AI models with APIs. You can design drag-and-drop visual workflows, use templates, and even tap into Postman’s growing network of APIs and MCP servers.</li><li><strong>Flows, MCP &amp; AI Developer Tools:</strong> Postman supports experiments with AI models, evaluating multiple LLMs, generating prompts, and building test suites.</li><li><strong>Autocomplete, Suggestions &amp; Docs Assistance:</strong> Useful for writing tests, analyzing large responses, and keeping documentation in sync.</li></ul><p><strong>Limitations:</strong></p><ul><li>Test-case generation isn’t fully automatic: Postbot can help, but often requires manual tweaking.</li><li>Workflows like chaining APIs, extracting variables, or setting up environments still require a lot of manual setup.</li><li>Postman’s depth comes with complexity, meaning a steeper learning curve and heavier overhead for new users.</li></ul><h3>Thunder Client: AI Features &amp; Strengths</h3><p>Thunder Client takes a different approach: <strong>lightweight API testing inside VS Code</strong>. Its philosophy is simplicity and speed.</p><ul><li><strong>Scriptless Testing &amp; GUI Assertions:</strong> Instead of writing code, you can define tests via dropdowns and GUI selectors (e.g., status codes, response fields).</li><li><strong>Core Features:</strong> Collections, environments, request history, and local storage — all the basics you need for quick API testing.</li><li><strong>AI Integration (Early Stages):</strong> Thunder Client has announced AI-generated tests as <strong>“coming soon”</strong>, meaning their AI feature set is still developing.</li><li><strong>CI/CD &amp; Git Sync:</strong> Useful for teams to collaborate, share collections, and integrate tests into workflows.</li></ul><p><strong>Limitations:</strong></p><ul><li>AI support is limited and still maturing.</li><li>Not ideal for complex workflows like chained requests or dynamic variable management.</li><li>Lacks no-code flow builders or advanced automation beyond simple assertions.</li></ul><h3>EchoAPI’s Edge</h3><p>Here’s where <strong>EchoAPI differentiates itself</strong>: AI isn’t just an add-on; it’s the <strong>engine</strong> of the testing workflow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/804/1*7e00zNtlcpORLz6l237jtw.png" /></figure><h3>At a Glance</h3><ul><li><strong>Postman</strong>: Best for power users who want deep control, integrations, and community-driven workflows.</li><li><strong>Thunder Client</strong>: Great for developers who prefer simplicity, speed, and testing inside VS Code.</li><li><strong>EchoAPI</strong>: Combines the best of both worlds: advanced AI automation, full workflow chaining, variable management, and a friendly UI that works for both coders and non-coders.</li></ul><h3>Why This Comparison Matters</h3><p>There are plenty of API tools out there. But the <strong>real value</strong> is in how much time you save, how many bugs you prevent, and how easily your team can scale test coverage.</p><ul><li>If you’re working on a big codebase, managing dozens of endpoints, or testing chained workflows, EchoAPI gives you automation that cuts down boilerplate and speeds up debugging.</li><li>If you’re working on a small project, Thunder Client might be enough for its simplicity. If you want the biggest ecosystem and integrations, Postman is still a solid choice.</li><li>But if you want <strong>AI-native testing</strong> that actually handles the heavy lifting, EchoAPI offers a strong alternative; one that’s already delivering mature, practical automation today.</li></ul><h3>Why This Matters</h3><p>EchoAPI’s AI features literally <strong>compress hours of manual work into seconds</strong>.</p><ul><li><strong>AI Test-case generator</strong>: No need to brainstorm test cases.</li><li><strong>AI Scripts</strong>: No need to code repetitive test scripts.</li><li><strong>AI Assertions</strong>: Validation without writing manual checks.</li><li><strong>Chained APIs</strong>: End-to-end workflow testing in one go.</li></ul><p>For <strong>developers</strong>, it means faster testing and less frustration. For <strong>QA engineers</strong>, it means scaling test coverage without extra effort. For <strong>teams</strong>, it means fewer bugs slip into production.</p><h3>Results Summary</h3><p>After spending time with EchoAPI’s AI-powered tools, here’s my honest takeaway:</p><ul><li>The <strong>auto test-case generation</strong> is the highlight; it gives you complete coverage in seconds.</li><li>The <strong>AI script &amp; assertion generator</strong> saves tons of time when building complex validations.</li><li>The <strong>multi-API chaining</strong> feels like real-world automation without the complexity of writing a giant test framework yourself.</li></ul><h3>Wrapping Up</h3><p>EchoAPI with AI isn’t just another API client. It’s like having a <strong>QA assistant</strong> sitting beside you.</p><p>Instead of writing, organizing, and debugging everything manually, you just <strong>guide the AI</strong> and let it handle the heavy lifting.</p><p><em>That’s my full walkthrough of EchoAPI’s AI features. Have you tried AI-assisted testing yet?</em></p><p><strong>That’s it for now…</strong></p><p><strong>Keep Coding✌✌</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ec329c6918a8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[BrightData’s MCP: Fetch Real-time Web Data For LLMs(AI Agents)]]></title>
            <link>https://geekpython.medium.com/brightdatas-mcp-fetch-real-time-web-data-for-llms-ai-agents-feeb7f11f64f?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/feeb7f11f64f</guid>
            <category><![CDATA[mcp-client]]></category>
            <category><![CDATA[mcp-server]]></category>
            <category><![CDATA[web-scraping]]></category>
            <category><![CDATA[web-scraping-service]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Thu, 05 Jun 2025 15:21:56 GMT</pubDate>
            <atom:updated>2025-06-05T15:26:53.731Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0IohH4bmbnckIxJJJMhWBg.png" /><figcaption><strong>Author: GeeKPython</strong></figcaption></figure><p>In the world of artificial intelligence, especially large language models (LLMs), there’s a quiet revolution happening. It’s called <strong>Model Context Protocol</strong>, or simply <strong>MCP</strong>. You may have encountered the term in AI communities or tool integrations, but what exactly is MCP, and why should you care?</p><p>Let’s break it down, step by step, and show you how this simple yet powerful concept is solving one of the biggest headaches in AI, which is accessing real-time web data.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fx4dY81COaI0%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dx4dY81COaI0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fx4dY81COaI0%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/9242327cd5904ff65015ab09a5492eba/href">https://medium.com/media/9242327cd5904ff65015ab09a5492eba/href</a></iframe><h3>What Is MCP (Model Context Protocol)?</h3><p>Imagine you’ve trained a brilliant AI that knows history, science, programming, and can even write poetry. But ask it what’s trending on Twitter or what the latest YouTube comments are on a viral video, and you’ll likely get outdated, static responses. Why? Because LLMs are not built for real-time web interaction out of the box.</p><p>That’s where <strong>MCP</strong> steps in.</p><p><strong>MCP</strong> is an <strong>open standard</strong> that acts as a bridge between AI agents (like Claude, GPT, etc.) and real-time data sources like websites, APIs, or databases. Think of it as a universal translator that allows AI models to interact with the constantly-changing web securely and efficiently.</p><p>Instead of hardcoding access to different websites or APIs, MCP standardizes the way AI tools request data. It makes live data scraping and interaction much more streamlined and predictable — no more breaking scripts or bot detection errors.</p><h3>Why Do LLMs Struggle with Real-Time Data?</h3><p>Let’s address the elephant in the room.</p><p>LLMs like Claude, ChatGPT, and others are trained on massive datasets, but that training is <strong>static</strong>. This means the AI only knows what it was trained on. If you ask it for stock prices, breaking news, or fresh YouTube comments, it simply doesn’t know. Even worse, if it tries to guess, it might give you hallucinated or flat-out wrong answers.</p><p>Sure, you could plug in some APIs or attempt to write your own scraping scripts. But anyone who’s tried this knows how quickly you hit roadblocks:</p><ul><li><strong>JavaScript rendering</strong></li><li><strong>CAPTCHA and anti-bot detection</strong></li><li><strong>Proxy rotation and management</strong></li><li><strong>Data formatting inconsistencies</strong></li><li><strong>Rate limits</strong></li><li><strong>And much more…</strong></li></ul><p>To obtain a list of recent comments or product prices, you find yourself immediately immersed in backend development.</p><h3>A Real-World Example: When LLMs Fail</h3><p>Let me give you a quick example.</p><p>I asked Claude to fetch the latest comments from a YouTube video. It understood the request just fine… but couldn’t get the data. That’s because it doesn’t have native access to YouTube’s dynamic content. Claude can only interpret requests, but it can’t execute scraping logic on its own.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3CG8p-OozBmXPYXP.png" /></figure><p>This is a classic scenario where even the smartest LLMs fall short without real-time access tools.</p><h3>Building Your Own MCP? It’s Harder Than It Looks</h3><p>Technically, you can build your own MCP setup. You’d need to:</p><ul><li>Set up a scraping infrastructure.</li><li>Manage rotating proxies.</li><li>Handle headless browser environments.</li><li>Deal with dynamic web rendering.</li><li>Write adapters to plug data into your AI agent.</li></ul><p>Sounds doable at first, right? But trust me, you’ll soon find yourself debugging edge cases and fighting CAPTCHAs, all for a task that should feel simple. What starts as a weekend project quickly turns into a full-time scraping job.</p><p>That’s why most developers and AI researchers look for managed solutions.</p><h3>Bright Data’s MCP: Your AI’s Web Gateway</h3><p>This is where <strong>Bright Data’s MCP</strong> changes the game.</p><p>Bright Data, a leading data collection platform, has created a <strong>plug-and-play MCP service</strong> that gives your AI agent real-time access to the web without dealing with any of the complexity.</p><p>Here’s what it offers:</p><ul><li><strong>Unblockable access</strong> to websites like Amazon, YouTube, LinkedIn, Instagram, X (Twitter), Facebook, TikTok, Zillow, Reddit, and even Google.</li><li><strong>Built-in proxy handling</strong> — no need to rotate or manage IPs.</li><li><strong>Headless browser support</strong> — for rendering JavaScript-heavy pages.</li><li><strong>Remote browser APIs</strong> — so your agent can interact with pages like a real user.</li><li><strong>Plenty of ready-made scraping tools</strong> — for different websites and use cases.</li></ul><p>Everything is handled under the hood, and all you need to do is configure the access; no scraping code required.</p><h3>Let’s See It in Action</h3><p>I ran the same prompt with more tasks using Bright Data’s MCP: “Fetch the latest comments from a YouTube video and provide that data in a json file”.</p><p><strong>You can see that it worked and that too without any hassle.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*R4pZJAnC5nEa28yp.png" /></figure><p>Behind the scenes, Bright Data’s tools were doing all the heavy lifting: launching a browser, unlocking the page, pulling the data, formatting it, and sending it back to the AI agent.</p><p>No errors. No retries. Just results.</p><h3>How to Integrate Bright Data’s MCP with Claude Desktop</h3><p>Okay, now let’s go through how to actually <strong>connect Bright Data’s MCP</strong> with Claude Desktop. It’s simpler than you might think.</p><h3>Step 1: Install Node.js</h3><p>Make sure <a href="https://nodejs.org/en/download">Node.js</a> is installed on your system. You’ll need the npx command to run the MCP server.</p><h3>Step 2: Create a Bright Data Account</h3><p>Head over to <a href="https://brightdata.com">Bright Data</a> and sign up. Once your account is ready, log in to the user dashboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Qq8nRT0KqvQeARxt.png" /></figure><h3>Step 3: Prepare the Claude Desktop Configuration</h3><ol><li>Open the Claude desktop app</li><li>Click on <strong>File</strong> → <strong>Settings</strong> → <strong>Developer</strong> → <strong>Edit File</strong>.</li><li>This opens a file named claude-desktop-config.json.</li><li>Open it in your favorite text editor.</li></ol><h3>Step 4: Add MCP Configuration</h3><p>Now, go to the <a href="https://github.com/brightdata-com/brightdata-mcp">GitHub repository</a> provided by Bright Data and copy the MCP config code.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/873/0*6PCmQvzhWwuyeIo1.png" /></figure><p>Paste that into the claude-desktop-config.json file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vAnhJ6YPhE06vrt-.png" /></figure><h3>Step 5: Insert Your Real MCP Credentials</h3><p>Once you’ve pasted the configuration block into the claude-desktop-config.json file, it’s time to replace the placeholders with your actual values. This is what will connect your Claude Desktop app to Bright Data’s MCP system.</p><p>Here’s how to do it:</p><p><strong>Head to Your Bright Data Dashboard</strong><br>Log in to your Bright Data account and navigate to the <strong>Proxies &amp; Scraping</strong> section. This is where you’ll manage the tools needed to enable real-time scraping.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*alIiByb10FWV7f8P.png" /></figure><p><strong>Create a Scraping Zone</strong><br>Click the <strong>“Add”</strong> button and choose <strong>“Web Unlocker API”</strong> as your tool. Once created, you’ll see a dashboard showing your new zone.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KqB-LD1T8-nVv7xs.png" /></figure><p><strong>Copy the API Key</strong><br>You’ll find your <strong>API key</strong> in the “Account Settings” section. Alternatively, Bright Data may have emailed it to you when you signed up. Either way, copy this key and paste it into the relevant &quot;API_TOKEN&quot; field in your config file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lm9D5xUJZE6x2wrU.png" /></figure><p><strong>Add the Web Unlocker Zone Name</strong><br>Still in your dashboard, find the name of the <strong>Web Unlocker zone</strong> you just created. Copy it and paste it into the &quot;WEB_UNLOCKER_ZONE&quot; field in your configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cTkmceFeSvaN5kNu.png" /></figure><p><strong>Enable Remote Browsing with Browser API</strong><br>Want to supercharge your scraping setup with headless browser access? You can!<br>Just add a <strong>Browser API</strong> by clicking “Browser API” in the “Add” section.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BkAreaNWF35UdmTs.png" /></figure><ul><li>Give it a name — something like my-browser-api.</li><li>After it’s created, you’ll see a string.</li><li>Copy just the key part (not the full URL) and paste it into the &quot;BROWSER_AUTH&quot; field in your config.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*H-JUFgifqkeq0K9Q.png" /></figure><p>This optional step enables your AI agent to interact with websites that require full browser emulation, which is perfect for scraping JavaScript-heavy pages or logging into websites.</p><h3>Step 6: Restart Claude</h3><p>Save the configuration file, quit Claude Desktop, and restart the app.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MipX2WXKjsgjd_C7.png" /></figure><p>Once you open it again, you’ll see that all Bright Data MCP tools are now available to use directly within Claude. That’s it, now you’re ready to start scraping real-time web data with zero friction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*puau5SNkPH0rKrPk.png" /></figure><h3>The Big Picture</h3><p>The MCP isn’t just another API — it’s a protocol that aims to standardize how AI tools interact with dynamic data sources. And with platforms like Bright Data offering powerful plug-and-play integrations, you can start building real-time, context-aware agents without worrying about the plumbing.</p><p>Instead of writing your own scrapers, debugging browser sessions, or juggling proxies, you can focus on what matters: creating intelligent workflows that adapt to live information.</p><p>Whether you’re working on:</p><ul><li>Competitive intelligence</li><li>Social media monitoring</li><li>Real-time news summaries</li><li>E-commerce pricing trackers</li><li>AI research assistants</li></ul><p>…MCP opens up a whole new world of possibilities.</p><h3>Conclusion</h3><p>Large Language Models are impressive — but without live data, they’re limited. <strong>MCP is the missing puzzle piece</strong>, and Bright Data’s implementation makes it incredibly easy to adopt.</p><p>If you’re tired of watching your AI fumble on basic real-time tasks, give MCP a shot. It’s fast, powerful, and surprisingly easy to set up. And if you’re using Claude Desktop, the whole thing takes under 10 minutes to integrate.</p><p>Your AI just got a whole lot smarter.</p><p><strong>That’s all for now.</strong></p><p><strong>Keep Coding✌✌</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=feeb7f11f64f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[⚡Thunder Client Paywalls? No thanks. EchoAPI is free!]]></title>
            <link>https://geekpython.medium.com/thunder-client-paywalls-no-thanks-echoapi-is-free-73564a130880?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/73564a130880</guid>
            <category><![CDATA[api-testing]]></category>
            <category><![CDATA[api-testing-tools]]></category>
            <category><![CDATA[api]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Sat, 30 Nov 2024 15:32:07 GMT</pubDate>
            <atom:updated>2024-11-30T15:32:07.263Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aPPmKW6fvD8i0TAnt0aaVA.png" /></figure><p>You might have heard of Thunder Client, a REST API client extension for Visual Studio Code that allows developers to test APIs. Thanks to its lightweight nature, it quickly became the first choice for developers worldwide.</p><p>Thunder Client was once completely free, but that’s no longer the case. It has now transitioned to a paid model, pushing many key features behind a paywall. With the free plan offering limited functionality, users have few options left.</p><p>Thunder Client used to be a one-stop destination for developers to perform basic API testing and debugging. However, with the shift to a paid model, developers are increasingly turning to alternatives like Postman. Among these, <a href="https://marketplace.visualstudio.com/items?itemName=EchoAPI.echoapi-for-vscode"><strong>EchoAPI for Visual Studio Code</strong></a> stands out as a strong and worthy alternative.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*w_lBcNuGsTj25ASh.png" /></figure><h3>Why EchoAPI is a Better Alternative?</h3><p>While EchoAPI and Thunder Client share some common features, EchoAPI excels in the following areas:</p><ul><li><strong>Completely Free</strong>: EchoAPI is entirely free to use, with a commitment to remain so.</li><li><strong>Ultra-Lightweight</strong>: Its minimalistic design ensures that it doesn’t burden your system.</li><li><strong>No Login Required</strong>: Start testing immediately without the need for sign-ups or logins.</li><li><strong>Scratchpad Feature</strong>: Dive straight into testing APIs without any setup.</li><li><strong>Postman Script Compatibility</strong>: Migrating from Postman? No problem! EchoAPI supports Postman script syntax, so you don’t need to learn anything new.</li><li><strong>Collaboration Ready</strong>: Enjoy team workflows for shared testing without extra costs.</li><li><strong>Unlimited Requests</strong>: There’s no limit to the number of requests you can send.</li><li><strong>Unlimited Collection Runs</strong>: Run your collections as many times as needed, without restrictions.</li><li><strong>Local Storage</strong>: Store your data securely and access it from anywhere.</li></ul><p>Let’s explore these features in detail.</p><h3>Install EchoAPI</h3><p>Getting started with EchoAPI is easy. Simply install it from the Visual Studio Code extensions marketplace:</p><p>Search for <strong>“EchoAPI”</strong>. Click on the first result and hit <strong>Install</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3biIKoNX18j3DjDD.png" /></figure><p>Once installed, the EchoAPI logo will appear in the sidebar, giving you access to the extension’s user interface (UI).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6BN4QoKnIJ6Jl4u6.png" /></figure><p>Now we can start testing our APIs. Let’s take a tour of the UI and explore EchoAPI functionalities.</p><h3>Get Started with EchoAPI</h3><p>EchoAPI provides demo APIs to help you explore its features. Let’s use these demo APIs for testing and debugging before moving on to testing our own.</p><p>For example, the first API in the <strong>“Samples”</strong> collection creates a new user with fields like id, username, firstName, lastName, email, password, phone, and userStatus.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wmbHihrHZKusxCG0.png" /></figure><p>Let’s tweak this data and send a request to the server.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DVJDMq_2mQsBkFGF.png" /></figure><p>After sending the request, we can see a successful response (HTTP status code <strong>200</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hnyHULpy_YUa34EJ.png" /></figure><p>Here, we can also test Server-sent Events (SSE) requests, which are used to send real-time updates to clients. These are often used in web applications for real-time notifications, live data feeds, or status updates.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QEP1SkozUtEJfzjS.png" /></figure><h3>Running Collections Without Limit</h3><p>One of EchoAPI’s standout features is its unlimited collection runs. Here’s how you can run a collection:</p><p>Navigate to the <strong>Tests</strong> tab and select the collection or folder containing your requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZiGVn53GbMnGOGos.png" /></figure><p>In this case, we selected the <strong>“Samples”</strong> collection and then clicked on the <strong>Run All</strong> button. It brings up this interface where we can set configurations to the test.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tEEfvIFKPZcaoSDf.png" /></figure><p>You can configure the following options:</p><ul><li><strong>Iterations</strong>: Specify how many times the test should execute.</li><li><strong>Intervals</strong>: Set time intervals between each request.</li><li><strong>Data</strong>: Upload a .csv file containing test data.</li><li><strong>Environment</strong>: Choose the testing environment with pre-configured environment variables.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fzHC9iplAaXqQQ2v.png" /></figure><p>In this case, the iterations are set to 3, and the interval between the execution of each request is set to 1000 ms (1 second).</p><p>Once configured, click <strong>Run</strong> to execute the collection. This will start executing requests with the specified configurations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*od-F9wKi0MYRHYvU.png" /></figure><p>A report is generated showing the stats of the performed test and if we click on any request, we’ll see an individual report of the request.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CEHxv6xb6ev0SUrc.png" /></figure><h3>Setting up Environment Variable</h3><p>We can set up environment variables like API keys, URLs, authentication tokens, etc. Depending on the sensitivity of the data, these variables can be used in the header or body of the request.</p><p>Let’s see how we can set up an environment in EchoAPI.</p><p>Access the environment setup interface via the <strong>three dots menu</strong> in the top left corner or directly from the request interface.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OpCmGOyJp8DZXkfU.png" /></figure><p>Click on the <strong>“New Environment”</strong> button to set up a new environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RvMRhBv5qrB8BU_P.png" /></figure><p>We can set a name for the environment, the base URL of the server, and environment variables.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*aRtTKw9PRl-Aetxx.png" /></figure><h3>Testing APIs</h3><p>Here’s a practical example using a Python FastAPI application with three routes. We’ll test these routes using EchoAPI.</p><pre>from fastapi import FastAPI, HTTPException, Depends<br>from pydantic import BaseModel<br>from typing import List<br>from fastapi.security import OAuth2PasswordBearer<br>import jwt<br>import datetime<br><br>app = FastAPI()<br>oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;token&quot;)<br><br>key = &#39;secretkey&#39;<br><br>books = []<br>current_id = 1<br><br>class Book(BaseModel):<br>    title: str<br>    author: str<br>    price: float<br><br>class BookOut(Book):<br>    id: int<br><br>def authenticate_user(token: str = Depends(oauth2_scheme)):<br>    try:<br>        payload = jwt.decode(token, key, algorithms=[&quot;HS256&quot;])<br>        return payload<br>    except jwt.ExpiredSignatureError:<br>        raise HTTPException(status_code=401, detail=&quot;Token has expired&quot;)<br>    except jwt.InvalidTokenError:<br>        raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)<br><br>@app.post(&quot;/token&quot;)<br>def get_token():<br>    expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)<br>    token = jwt.encode({&quot;exp&quot;: expiration}, key, algorithm=&quot;HS256&quot;)<br>    return {&quot;access_token&quot;: token, &quot;token_type&quot;: &quot;bearer&quot;}<br><br>@app.post(&quot;/books&quot;, response_model=BookOut)<br>def create_book(book: Book, user: dict = Depends(authenticate_user)):<br>    global current_id<br>    new_book = book.model_dump()<br>    new_book[&quot;id&quot;] = current_id<br>    books.append(new_book)<br>    current_id += 1<br>    return new_book<br><br>@app.get(&quot;/books&quot;, response_model=List[BookOut])<br>def list_books(user: dict = Depends(authenticate_user)):<br>    return books</pre><h3>Setting up EchoAPI for Testing</h3><p>First, we can create a folder, let’s say <strong>Books</strong>, to make our requests more organized.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BGE2FW47I6jB36bg.png" /></figure><p>Next, we need to create individual requests within the <strong>“Books”</strong> folder.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qVc-uT37kXhbDtxk.png" /></figure><p>We created three requests as shown in the image below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*w1jtivnjyNXGil4d.png" /></figure><p>According to the code, we need to generate an authentication token, so we need to send a POST request to http://127.0.0.1:8000/token to obtain an auth token and then we can use this token in other requests to authenticate a user.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EhYmK-J8sR8Fioj-.png" /></figure><h3>Authentication</h3><p>If we try to hit this URL (http://127.0.0.1:8000/books) to create a book without passing the auth token, we’ll get an error in the response.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gFGFuSWhBbcda8sW.png" /></figure><p>We need to pass the previously generated auth token in the <strong>“Auth”</strong> section.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ublWDwQTazrny8li.png" /></figure><p>EchoAPI supports various authentication methods such as API Key, Basic Auth, and JWT Bearer. You can choose the type of authentication required in your project.</p><h3>Debugging APIs</h3><p>We can debug the APIs from the <strong>“Post-response”</strong> section. In this section, we can write custom scripts (<strong>Postman script</strong>) and set assertions to debug APIs, extract variables for dynamic testing, and simulate delays in API execution.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sn-9f212vZvsO6bI.png" /></figure><p>Let’s perform a simple debugging. We are setting an assertion for the response code equal to 200 and deliberately making a mistake in the request.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Dg4yD-VAO-Av485X.png" /></figure><p>We can see detail in the response indicating that the price field is missing and it returned the response code 422, which is not equal to 200, so the assertion failed.</p><p>This is not limited to response code only, we can set assertions for different target objects.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4y_gx9oeQLqiW2Rk.png" /></figure><h3>More Features</h3><p><strong>Server sent event Requests</strong>:</p><p>Server-sent events (SSE) enable a server to push real-time updates to the client over a single HTTP connection. They are widely used for real-time notifications, live data feeds, status updates, or chat applications.</p><p>With <strong>Thunder Client</strong>, creating and testing SSE requests is locked behind a paywall. This makes it less appealing for developers who rely on free tools for basic functionality.</p><p><strong>EchoAPI</strong>, however, provides full support for SSE requests <strong>free of charge</strong>. Here’s how EchoAPI simplifies SSE testing:</p><ol><li><strong>Ease of Setup</strong>: You can create and test SSE requests directly within EchoAPI’s user interface, without any advanced configuration.</li><li><strong>Real-Time Stream Support</strong>: It allows you to see real-time data streams as they are sent from the server.</li><li><strong>No Cost</strong>: Unlike Thunder Client, there are no charges or restrictions for using this feature.</li></ol><p>For example, you can set up an SSE endpoint in your API, like /notifications, and use EchoAPI to connect to it. EchoAPI will display live updates sent by the server in a clean and intuitive interface.</p><p><strong>Imports data from cURL:</strong></p><p>cURL is a command-line tool used to send HTTP requests. Developers often use cURL commands to test APIs quickly in a terminal. However, when transitioning to a GUI-based tool like EchoAPI, manually re-entering the details of API requests can be tedious and error-prone.</p><p>Here’s how it works:</p><ol><li><strong>Copy cURL Command</strong>: Simply copy the cURL command from your terminal or documentation.</li><li><strong>Paste in EchoAPI</strong>: Select the <strong>Import cURL</strong> option in EchoAPI, and paste the cURL command.</li><li><strong>Automatic Conversion</strong>: EchoAPI automatically parses the command and converts it into a structured API request within its interface.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SRslEMFL7-ptHebB.png" /></figure><p><strong>Import data from Postman:</strong></p><p>Postman is one of the most popular API testing tools, often used for creating and managing collections of API requests. If you’re moving from Postman to EchoAPI, re-creating collections from scratch can be a daunting task.</p><p>Here’s how it works:</p><ol><li><strong>Export from Postman</strong>: In Postman, go to the collection you want to migrate, click on the options menu (three dots), and choose <strong>Export</strong>. Postman will generate a JSON file containing all the details of the collection.</li><li><strong>Import to EchoAPI</strong>: Select the <strong>Import from Postman</strong> option in EchoAPI, and upload the Postman JSON file.</li><li><strong>Seamless Integration</strong>: EchoAPI will parse the file and recreate the collection, including all requests, headers, parameters, and body configurations.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Hpzi-xoyi4Bwz3Qr.png" /></figure><h3>Team Collaboration and Data Sync</h3><p>To collaborate with the team and sync data in EchoAPI, we need to sign up. After signing up for EchoAPI, we can push data to EchoAPI storage easily.</p><p>Click on the cloud icon and then select the workspace folder where the data will be stored and then select the collection to push to EchoAPI.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GpdeRz2s4EW5Gl1n.png" /></figure><p>Now we can easily access the data from the <a href="https://www.echoapi.com/download">EchoAPI desktop app</a> or <a href="https://app.echoapi.com/">Webapp</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*uEz0MydhITEXBsjO.png" /></figure><p>We can invite our team members through URL in the EchoAPI web app or desktop app. Click on the <strong>“Invite”</strong> button in the top right corner, and then click on the <strong>“Copy Link”</strong> button to copy the invitation link.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pLdSE3YdoZEGR4fZ.png" /></figure><h3>Conclusion</h3><p>While Thunder Client once ruled as a lightweight API testing extension for Visual Studio Code, its shift to a paid model has left many developers searching for viable alternatives. EchoAPI emerges as a strong contender, offering a feature-rich, completely free, and ultra-lightweight solution tailored for seamless API testing and debugging.</p><p>With its no-login requirement, Postman script compatibility, limitless testing capabilities, and team collaboration features, EchoAPI sets itself apart as a powerful tool for both individual developers and teams. Its intuitive interface, support for server-sent events, and extensive customization options make it a go-to choice for API testing directly within VS Code.</p><p>If you’re looking for an efficient, no-cost alternative to Thunder Client that doesn’t compromise on functionality, EchoAPI is worth exploring.</p><p><strong>That’s all for now.</strong></p><p><strong>Keep Coding✌✌.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=73564a130880" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Different Ways to Stream Videos on the Frontend in FastAPI]]></title>
            <link>https://levelup.gitconnected.com/different-ways-to-stream-videos-on-the-frontend-in-fastapi-91f328225688?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/91f328225688</guid>
            <category><![CDATA[web]]></category>
            <category><![CDATA[streaming]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[fastapi]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Thu, 14 Nov 2024 21:29:00 GMT</pubDate>
            <atom:updated>2024-11-14T21:29:00.031Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d5dx6PSUCM3gb3utUTuc5A.png" /><figcaption><strong>Source: </strong><a href="https://geekpython.in/stream-video-to-frontend-in-fastapi"><strong>GeekPython</strong></a></figcaption></figure><p>FastAPI is a fast and modern web framework known for its support for asynchronous REST API and ease of use.</p><p>In this article, we’ll see how to stream videos on frontend in FastAPI.</p><h3>StreamingResponse</h3><h3>Stream Local Video</h3><p>FastAPI provides a StreamingResponse class that is dedicated to streaming purposes. The StreamingResponse class takes a generator or iterator and streams the response.</p><p>Here’s a simple example of streaming local video to the browser.</p><pre># localvid.py<br>from fastapi import FastAPI<br>from fastapi.responses import StreamingResponse<br><br>app = FastAPI()<br><br># Video path<br>vid_path = &#39;sample_video.mp4&#39;<br><br># Function to stream local video<br>def stream_local_video():<br>    with open(vid_path, &#39;rb&#39;) as vid_file:<br>        yield from vid_file<br><br># Path to stream video<br>@app.get(&quot;/&quot;)<br>def video_stream():<br>    return StreamingResponse(stream_local_video(), media_type=&#39;video/mp4&#39;)</pre><p>We imported the StreamingResponse class from the fastapi.responses module.</p><p>In the video_stream() path operation function, we returned the response using StreamingResponse. We passed the generator function stream_local_video() and media_type as an argument to the StreamingResponse class.</p><p>The generator function stream_local_video() reads the bytes of the video and yield from iterates over the bytes and each part iterated is then yielded.</p><p>The following command will run the server and stream the video on http://127.0.0.1:8000/.</p><pre>&gt; fastapi dev localvid.py<br><br> ╭────────── FastAPI CLI - Development mode ───────────╮<br> │                                                     │<br> │  Serving at: http://127.0.0.1:8000                  │<br> │                                                     │<br> │  API docs: http://127.0.0.1:8000/docs               │<br> │                                                     │<br> │  Running in development mode, for production use:   │<br> │                                                     │<br> │  fastapi run                                        │<br> │                                                     │<br> ╰─────────────────────────────────────────────────────╯</pre><h3>Stream Online Video</h3><pre># onlinevid.py<br>from fastapi import FastAPI<br>from fastapi.responses import StreamingResponse<br>import requests<br><br>app = FastAPI()<br><br># Video URL<br>vid_url = &#39;https://cdn.pixabay.com/video/2023/07/28/173530-849610807_large.mp4&#39;<br><br># Function to stream online video<br>def stream_online_video(url):<br>    response = requests.get(url, stream=True)<br>    for portion in response.iter_content(chunk_size=1024*1024):<br>        yield portion<br><br># Path to stream video<br>@app.get(&quot;/&quot;)<br>def video_stream():<br>    return StreamingResponse(stream_online_video(vid_url), media_type=&#39;video/mp4&#39;)</pre><p>In this example, we used the requests library to fetch the content of the video from the URL and we set the stream=True (avoids reading the video at once into memory). Then we iterated and yielded the video content in chunks (1024 bytes at a time).</p><p>When we run the server, it will serve the video from the URL.</p><p>Instead of a hardcoded URL, we can specify the URL in the endpoint.</p><pre>from fastapi import FastAPI<br>from fastapi.responses import StreamingResponse<br>import requests<br><br>app = FastAPI()<br><br># Function to stream online video<br>def stream_online_video(url):<br>    response = requests.get(url, stream=True)<br>    for portion in response.iter_content(chunk_size=1024*1024):<br>        yield portion<br><br># Path to stream video<br>@app.get(&quot;/&quot;)<br>def video_stream(url):<br>    return StreamingResponse(stream_online_video(url), media_type=&#39;video/mp4&#39;)</pre><p>In this example, we modified the path operation function video_stream() to accept a URL.</p><p>Now we can specify the URL of the video in the endpoint in the following format.</p><p>http://127.0.0.1:8000/?url=https://cdn.pixabay.com/video/2023/07/28/173530-849610807_large.mp4</p><blockquote><em>We can also make the path operation function asynchronous using </em><em>async.</em></blockquote><h3>FileResponse</h3><h3>Stream Local Video</h3><p>The FileResponse class simply takes a file and streams the response.</p><pre>from fastapi import FastAPI<br>from fastapi.responses import FileResponse<br><br>app = FastAPI()<br><br># Video path<br>vid_path = &#39;video.mp4&#39;<br><br># Path to stream video<br>@app.get(&quot;/&quot;)<br>def video_stream():<br>    return FileResponse(vid_path, media_type=&#39;video/mp4&#39;)</pre><p>In the above example, we simply passed a video file in the FileResponse class and returned the response.</p><p>We didn’t read and iterate over the bytes of the video to stream it like we did in the case of StreamingResponse.</p><p>The FileResponse class is ideal for relatively small or medium-sized files as it loads the file in the memory. Large files will consume more memory.</p><h3>Stream Online Video</h3><p>Streaming video from the URL using FileResponse isn’t the same as streaming the local video. As we know, the FileResponse class takes a file and streams it.</p><pre>from fastapi import FastAPI<br>from fastapi.responses import FileResponse<br>import requests<br>import os<br><br>app = FastAPI()<br><br># Video URL<br>vid_url = &#39;https://cdn.pixabay.com/video/2023/07/28/173530-849610807_large.mp4&#39;<br>local_vid = &#39;sample_video.mp4&#39;<br><br># Function to save video from URL<br>def save_as_file(url, path):<br>    if not os.path.exists(path):<br>        response = requests.get(url)<br>        with open(&#39;sample_video.mp4&#39;, &#39;wb&#39;) as vid:<br>            vid.write(response.content)<br><br>save_as_file(vid_url, local_vid)<br><br># Path to stream video<br>@app.get(&quot;/&quot;)<br>def video_stream():<br>    return FileResponse(local_vid, media_type=&#39;video/mp4&#39;)</pre><p>In this example, instead of streaming the local video, we streamed the video from the URL using FileResponse.</p><p>This isn’t the best practice because we first saved the video from the URL in the local machine and then streamed the video.</p><p>The FileResponse class can be best utilized for local files, not web content.</p><p>That was all about streaming video directly to the browser but we don’t want to do that every time, instead, we want the video to be streamed on our website’s landing page.</p><h3>Stream Video on Frontend</h3><p>So, <strong>how to stream video on the frontend using FastAPI?</strong> We need to create a frontend using HTML and then stream the video.</p><p>First, create a directory named serve_video or whatever you want to name it, and create the following files and sub-directories.</p><pre>serve_video/<br>   - templates/<br>      - display_video.html<br>   - app.py</pre><p>The app.py file will contain our backend code and the display_video.html file will contain frontend code.</p><p>app.py</p><pre>from fastapi import FastAPI, Request<br>from fastapi.responses import StreamingResponse, HTMLResponse<br>from fastapi.templating import Jinja2Templates<br>import requests<br><br>app = FastAPI()<br>templates = Jinja2Templates(directory=&quot;templates&quot;)<br><br>vid_urls = [<br>    &quot;https://cdn.pixabay.com/video/2023/07/28/173530-849610807_large.mp4&quot;,<br>    &#39;https://cdn.pixabay.com/video/2024/03/31/206294_large.mp4&#39;,<br>    &#39;https://cdn.pixabay.com/video/2023/10/11/184510-873463500_large.mp4&#39;,<br>    &#39;https://cdn.pixabay.com/video/2023/06/17/167569-837244635_large.mp4&#39;<br>]<br><br># Stream the video from the URL<br>def stream_video(url):<br>    response = requests.get(url, stream=True)<br>    for portion in response.iter_content(chunk_size=1024*1024):<br>        yield portion<br><br># Endpoint to render the HTML template with the video player<br>@app.get(&quot;/&quot;, response_class=HTMLResponse)<br>async def video_template(request: Request):<br>    return templates.TemplateResponse(&quot;display_video.html&quot;, {&quot;request&quot;: request})<br><br># Endpoint to stream the video<br>@app.get(&quot;/video/{video_id}&quot;)<br>async def video_stream(vid_id: int):<br>    if 0 &lt;= vid_id &lt; len(vid_urls):<br>        return StreamingResponse(stream_video(vid_urls[vid_id]), media_type=&quot;video/mp4&quot;)<br>    else:<br>        return HTMLResponse(&quot;Video not found&quot;, status_code=404)</pre><p>In this example, we are streaming multiple videos on the frontend. We have a list of video URLs stored within vid_urls.</p><p>We have a stream_video() generator function that yields the video in smaller chunks.</p><p>We created an asynchronous path operation function video_template() that returns a response from HTML (@app.get(&quot;/&quot;, response_class=HTMLResponse)) file.</p><p>We are serving HTML from a template stored within the templates directory, so, we set up a Jinja2 template directory where HTML templates are stored (templates = Jinja2Templates(directory=&quot;templates&quot;)). This is where FastAPI will look for .html files.</p><p>Then we returned the response using templates.TemplateResponse(&quot;display_video.html&quot;, {&quot;request&quot;: request}). We passed our HTML template display_video.html and along with it, a dictionary containing Request object ({&quot;request&quot;: request}). This Request object provides information about the incoming HTTP request, such as headers, cookies, and URLs, which can be accessed within the HTML template.</p><p>Next, we created an endpoint (@app.get(&quot;/video/{video_id}&quot;)) to stream individual videos based on the ID in the vid_ulrs. The video_stream(vid_id: int) path operation functions accept the index of the video in vid_urls.</p><p>Within the video_stream(), the if condition checks if the vid_id is within the range, if not, then raises an error otherwise the video is streamed based on the specified index.</p><p>display_video.html</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html lang=&quot;en&quot;&gt;<br>&lt;head&gt;<br>    &lt;meta charset=&quot;UTF-8&quot;&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;<br>    &lt;title&gt;Stooooockzz&lt;/title&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>    &lt;h1&gt;What we do?&lt;/h1&gt;<br>    &lt;p&gt;We create awesome stock videos for free usage&lt;/p&gt;<br>    &lt;h3&gt;Video Samples&lt;/h3&gt;<br>    &lt;div style=&quot;display: flex; justify-content: space-evenly;&quot;&gt;<br>        &lt;video width=&#39;20%&#39; height=&#39;30%&#39; style=&quot;border-radius: 10px;&quot; controls autoplay&gt;<br>            &lt;source src=&quot;/video/0&quot; type=&quot;video/mp4&quot;&gt;<br>        &lt;/video&gt;<br>        &lt;video width=&#39;20%&#39; height=&#39;30%&#39; style=&quot;border-radius: 10px;&quot; controls autoplay&gt;<br>            &lt;source src=&quot;/video/1&quot; type=&quot;video/mp4&quot;&gt;<br>        &lt;/video&gt;<br>        &lt;video width=&#39;20%&#39; height=&#39;30%&#39; style=&quot;border-radius: 10px;&quot; controls autoplay&gt;<br>            &lt;source src=&quot;/video/2&quot; type=&quot;video/mp4&quot;&gt;<br>        &lt;/video&gt;<br>        &lt;video width=&#39;20%&#39; height=&#39;30%&#39; style=&quot;border-radius: 10px;&quot; controls autoplay&gt;<br>            &lt;source src=&quot;/video/3&quot; type=&quot;video/mp4&quot;&gt;<br>        &lt;/video&gt;<br>    &lt;/div&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>This HTML file contains multiple &lt;video&gt; tags with the source pointing to the /video/{video_id} endpoint that streams the video based on ID.</p><p>Here, we passed &lt;source src=&quot;/video/0&quot; type=&quot;video/mp4&quot;&gt; within the &lt;video&gt; tag that represents streaming the first video from the vid_urls (in app.py) list. Similarly, we specified the same source but changed the ID within each &lt;video&gt; tag.</p><pre>&gt; fastapi dev app.py</pre><p>Upon running the server using the above command, we get the following response on the frontend.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*f7id1wFE8Vq21qlO.png" /><figcaption><strong>Resulted Webpage</strong></figcaption></figure><h3>Conclusion</h3><p>We’ve used StreaminResponse and FileResponse classes to stream local and web videos directly on the browser, and along with this we’ve also streamed multiple videos using StreamingResponse which was rendered using HTML files on the browser.</p><p>In this article, we’ve used URLs of the video residing on the internet but you can also fetch URLs from databases, local files, disks, etc.</p><p><strong>That’s all for now.</strong></p><p><strong>Keep Coding✌✌.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=91f328225688" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/different-ways-to-stream-videos-on-the-frontend-in-fastapi-91f328225688">Different Ways to Stream Videos on the Frontend in FastAPI</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Difference between eval and exec in Python]]></title>
            <link>https://geekpython.medium.com/difference-between-eval-and-exec-in-python-06194dfb6dd8?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/06194dfb6dd8</guid>
            <category><![CDATA[function]]></category>
            <category><![CDATA[comparison]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Thu, 14 Nov 2024 11:39:22 GMT</pubDate>
            <atom:updated>2024-11-14T11:39:22.382Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pOFZMxP1Amvcd7Jis17-WQ.png" /><figcaption><strong>Source: </strong><a href="https://geekpython.in/exec-and-eval"><strong>GeekPython</strong></a></figcaption></figure><p>Both functions have a common objective: <strong>to execute Python code from the string input or code object</strong>. Even though they both have the same objective, exec() and eval() are not the same.</p><h3>Return Values</h3><p>The exec() function doesn&#39;t return any value whereas the eval() function returns a value computed from the expression.</p><pre>expression = &quot;3 + 5&quot;<br><br>result_eval = eval(expression)<br>print(result_eval)<br><br>result_exec = exec(expression)<br>print(result_exec)</pre><p>When we run this code, we’ll get 8 and None. This means that eval(expression) evaluated the result and stored it inside result_eval whereas exec(expression) returned nothing, so the value None gets stored into result_exec.</p><pre>8<br>None</pre><h3>Execution</h3><p>The exec() function is capable of executing multi-line and multi-statment code, it doesn&#39;t matter whether the code is simple, complex, has loops and conditions, classes, and functions.</p><p>On the other hand, the eval() function is restricted to executing the single-line code which might be a simple or complex expression.</p><pre>expression = &quot;&quot;&quot;<br>for x in range(5):<br>    print(x, end=&quot; &quot;)<br>&quot;&quot;&quot;<br><br>exec(expression)<br>eval(expression)</pre><p>Look at this code, we have a multi-line code that prints numbers up to the given range. The exec(expression) will execute it and display the result but the eval(expression) will display the error.</p><pre>0 1 2 3 4<br>SyntaxError: invalid syntax</pre><p>However, if we convert the same expression into a single line as the following, the eval(expression) will not throw any error.</p><pre>expression = &quot;[print(x, end=&#39; &#39;) for x in range(5)]&quot;<br>eval(expression)<br><br>--------------------<br>0 1 2 3 4</pre><h3>Summary</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/802/1*3eeSAC2nafliugt3Ci-ydA.png" /></figure><p>🏆<strong>Other articles you might be interested in if you liked this one</strong></p><p>✅<a href="https://geekpython.in/exec-function-in-python">Execute complex code blocks from string input using exec() function</a>.</p><p>✅<a href="https://geekpython.in/template-inheritance-in-flask">Template inheritance in Flask</a>.</p><p>✅<a href="https://geekpython.in/type-hinting-in-python">Type hints in Python — Callable objects, Return values, and more</a>?</p><p>✅<a href="https://geekpython.in/positional-and-keyword-arguments-in-python">Best Practices: Positional and Keyword Arguments in Python</a></p><p>✅<a href="https://geekpython.in/yield-keyword-in-python">Yield Keyword in Python with Examples</a>?</p><p>✅<a href="https://geekpython.in/build-websocket-server-and-client-using-python">Create a WebSocket Server and Client in Python</a>.</p><p><strong>That’s all for now</strong></p><p><strong>Keep Coding✌✌</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=06194dfb6dd8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[10 Useful yet Rarely Used OS Functions in Python]]></title>
            <link>https://geekpython.medium.com/10-useful-yet-rarely-used-os-functions-in-python-3c44d4723290?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/3c44d4723290</guid>
            <category><![CDATA[python-tips]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[os]]></category>
            <category><![CDATA[function]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Sun, 10 Nov 2024 08:50:36 GMT</pubDate>
            <atom:updated>2024-11-10T08:50:36.265Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pIxDPDaurWPS8a4dZimZ4g.png" /><figcaption><strong>Source: </strong><a href="https://geekpython.in/useful-yet-rarely-used-os-functions"><strong>GeekPython</strong></a></figcaption></figure><p>You must have used functions provided by the os module in Python several times in your projects. These could be used to create a file, walk down a directory, get info on the current directory, perform path operations, and more.</p><p>In this article, we’ll discuss the functions that are as useful as any function in the os module but are rarely used.</p><h3>1. os.path.commonpath()</h3><p>When working with multiple files that share a common directory structure, you might want to find the longest shared path. os.path.commonpath() does just that. This can be helpful when organizing files or dealing with different paths across environments.</p><p>Here’s an example:</p><pre>import os<br><br>paths = [&#39;/user/data/project1/file1.txt&#39;, &#39;/user/data/project2/file2.txt&#39;]<br>common_path = os.path.commonpath(paths)<br>print(&quot;Common Path:&quot;, common_path)</pre><p>This code will give us the common path shared by these two paths.</p><pre>Common Path: /user/data</pre><p>You can see that os.path.commonpath() takes a list of path names, which might be impractical to manually write them down.</p><p>In that case, it is best to iterate over all of the directories, subdirectories, and file names and then look for the common path.</p><pre>import os<br><br>def get_file_paths(directory, file_extension=None):<br>    # Collect all file paths in the directory (and subdirectories, if any)<br>    file_paths = []<br>    for root, dirs, files in os.walk(directory):<br>        for file in files:<br>            if file_extension is None or file.endswith(file_extension):<br>                file_paths.append(os.path.join(root, file))<br>    return file_paths<br><br><br># Specify the root directory to start from<br>directory_path = &#39;D:/SACHIN/Pycharm/Flask-Tutorial&#39;<br><br># If you want to filter by file extension<br>file_paths = get_file_paths(directory_path, file_extension=&#39;.html&#39;)<br><br># Find the common path among all files<br>if file_paths:<br>    common_path = os.path.commonpath(file_paths)<br>    print(&quot;Common Path:&quot;, common_path)<br>else:<br>    print(&quot;No files found in the specified directory.&quot;)</pre><p>In this example, the function get_file_paths() traverses a directory from top to bottom and appends all the paths found in the file_paths list. This function optionally takes a file extension if we want to look out for specific files.</p><p>Now we can easily find the common path of any directory.</p><pre>Common Path: D:\SACHIN\Pycharm\Flask-Tutorial\templates</pre><h3>2. os.scandir()</h3><p>If you’re using os.listdir() to get the contents of a directory, consider using os.scandir() instead. It’s not only faster but also returns <a href="https://docs.python.org/3/library/os.html#os.DirEntry">DirEntry</a> objects, which provide useful information like file types, permissions, and whether the entry is a file or a directory.</p><p>Here’s an example:</p><pre>import os<br><br>with os.scandir(&#39;D:/SACHIN/Pycharm/osfunctions&#39;) as entries:<br>    for entry in entries:<br>        print(f&quot;{entry.name} : \n&quot;<br>              f&quot;&gt;&gt;&gt;&gt; Is File: {entry.is_file()} \n&quot;<br>              f&quot;&gt;&gt;&gt;&gt; Is Directory: {entry.is_dir()}&quot;)</pre><p>In this example, we used os.scandir() and passed a directory and then we iterated over this directory and printed the info.</p><pre>.idea : <br>&gt;&gt;&gt;&gt; Is File: False <br>&gt;&gt;&gt;&gt; Is Directory: True<br>main.py : <br>&gt;&gt;&gt;&gt; Is File: True <br>&gt;&gt;&gt;&gt; Is Directory: False<br>sample.py : <br>&gt;&gt;&gt;&gt; Is File: True <br>&gt;&gt;&gt;&gt; Is Directory: False</pre><h3>3. os.path.splitext()</h3><p>Let’s say you’re working with files and need to check their extension, you can get help from os.path.splitext() function. It splits the file path into the root and extension, which can help you determine the file type.</p><pre>import os<br><br>filename = &#39;report.csv&#39;<br>root, ext = os.path.splitext(filename)<br>print(f&quot;Root: {root} \n&quot;<br>      f&quot;Extension: {ext}&quot;)</pre><p><strong>Output</strong></p><pre>Root: report <br>Extension: .csv</pre><p>Look at some cases where paths can be weird, at that time how os.path.splitext() works.</p><pre>import os<br><br>filename = [&#39;.report&#39;, &#39;report&#39;, &#39;report.case.txt&#39;, &#39;report.csv.zip&#39;]<br>for idx, paths in enumerate(filename):<br>    root, ext = os.path.splitext(paths)<br>    print(f&quot;{idx} - {paths}\n&quot;<br>          f&quot;Root: {root} | Extension: {ext}&quot;)</pre><p><strong>Output</strong></p><pre>0 - .report<br>Root: .report | Extension: <br>1 - report<br>Root: report | Extension: <br>2 - report.case.txt<br>Root: report.case | Extension: .txt<br>3 - report.csv.zip<br>Root: report.csv | Extension: .zip</pre><h3>4. os.makedirs()</h3><p>There’s already a frequently used function that allows us to create directories. But what about when you create nested directories?</p><p>Creating nested directories can be a hassle with os.mkdir() since it only makes one directory at a time. os.makedirs() allows you to create multiple nested directories in one go, and the exist_ok=True argument makes sure it doesn’t throw an error if the directory already exists.</p><pre>import os<br><br>os.makedirs(&#39;project/data/files&#39;, exist_ok=True)<br>print(&quot;Nested directories created!&quot;)</pre><p>When we run this program, it will create specified directories and sub-directories.</p><pre>Nested directories created!</pre><p>If we run the above program again, it won’t throw an error due to exist_ok=True.</p><h3>5. os.replace()</h3><p>Similar to os.rename(), os.replace() moves a file to a new location, but it safely overwrites any existing file at the destination. This is helpful for tasks where you’re updating or backing up files and want to ensure that old files are safely replaced.</p><pre>import os<br><br>os.replace(src=&#39;main.py&#39;, dst=&#39;new_main.py&#39;)<br>print(&quot;File replaced successfully!&quot;)</pre><p>In this code, main.py file will be renamed to new_main.py just as os.rename() function but this operation is like take it all or nothing. It means the file replacement happens in a single, indivisible step, so either the entire operation succeeds or nothing changes at all.</p><pre>File replaced successfully!</pre><h3>6. os.urandom()</h3><p>For cryptographic purposes, you need a secure source of random data. os.urandom() generates random bytes suitable for things like generating random IDs, tokens, or passwords. It’s more secure than the random module for sensitive data.</p><p>os.urandom() uses randomness generated by the operating system you are using from various resources to make bytes (data) unpredictable.</p><p>In Windows, it uses BCryptGenRandom() to generate random bytes.</p><pre>import os<br><br>secure_token = os.urandom(16)  # 16 bytes of random data<br>print(&quot;Secure Token:&quot;, secure_token)<br>#Making it human-readable<br>print(&quot;Secure Token:&quot;, secure_token.hex())</pre><p><strong>Output</strong></p><pre>Secure Token: b&#39;\x84\xd6\x1c\x1bKB\x7f\xcd\xf6\xb7\xc4D\x92z\xe3{&#39;<br>Secure Token: 84d61c1b4b427fcdf6b7c444927ae37b</pre><h3>7. os.path.samefile()</h3><p>The os.path.samefile() function in Python is used to check if two paths refer to the <strong>same file</strong> or <strong>directory</strong> on the filesystem. It’s particularly helpful in scenarios where multiple paths might point to the same physical file, such as when dealing with symbolic links, hard links, or different absolute and relative paths to the same location.</p><pre>import os<br><br>is_same = os.path.samefile(&#39;/path/to/file1.txt&#39;, &#39;/different/path/to/symlink_file1.txt&#39;)<br>print(&quot;Are they the same file?&quot;, is_same)</pre><p>os.path.samefile() is designed to return True only if both paths reference the <strong>same file</strong> on disk, such as a file that’s hard-linked or symlinked to the same data on the filesystem.</p><h3>8. os.path.relpath()</h3><p>os.path.relpath() is a computation function that computes the relative path between two paths. This is particularly useful when building file paths dynamically or working with relative imports.</p><p>Consider the following example:</p><pre>import os<br><br># Target file path<br>target_path = &quot;D:/SACHIN/Pycharm/osfunctions/project/engine/log.py&quot;<br># Starting point<br>start_path = &quot;D:/SACHIN/Pycharm/osfunctions/project/interface/character/specific.py&quot;<br><br>relative_path = os.path.relpath(target_path, start=start_path)<br>print(relative_path)</pre><p>In this example, we have target_path which contains a path where we have to navigate and start_path contains a path from where we have to start calculating the relative path to target_path.</p><p>When we run this, we get the following output.</p><pre>..\..\..\engine\log.py</pre><p>This means we have to go up three directories and then down to engine/log.py.</p><h3>9. os.fsync()</h3><p>When we perform a file writing (file.write()) operation, the data isn’t saved to disk instantly instead the data is saved into the system’s buffer and if something unexpected happens before writing the data to the disk, the data gets lost.</p><p>os.fsync() forces the data to be written, ensuring data integrity. It’s especially useful in logging or when writing critical data that must not be lost.</p><pre>import os<br><br>with open(&#39;data.txt&#39;, &#39;w&#39;) as f:<br>    f.write(&quot;gibberish!&quot;)<br>    os.fsync(f.fileno())  # Ensures data is written to disk</pre><p>os.fsync(f.fileno()) is called to make sure the data is immediately written to the disk and not left in the buffer.</p><p>os.fsync() takes file descriptor that’s why we passed f.fileno() which is a unique integer assigned by the system to the file on which we are operating.</p><h3>10. os.get_terminal_size()</h3><p>If you’re creating CLI tools, formatting the output to fit the terminal width can make the output cleaner. os.get_terminal_size() gives you the current terminal width and height, making it easy to dynamically format content.</p><pre>import os<br><br>size = os.get_terminal_size()<br>print(f&quot;Terminal Width: {size.columns}, Terminal Height: {size.lines}&quot;)</pre><p>When we run this code in the terminal, we get the size of the terminal on which we are running this script.</p><pre>PS &gt; py sample.py<br>Terminal Width: 158, Terminal Height: 12</pre><p>Note: You may get an error when directly running the script on IDE where the program doesn’t have access to the terminal.</p><p>🏆<strong>Other articles you might be interested in if you liked this one</strong></p><p>✅<a href="https://geekpython.in/stream-video-to-frontend-in-fastapi">Streaming videos on the frontend in FastAPI</a>.</p><p>✅<a href="https://geekpython.in/circular-import-error-in-python">How to fix circular imports in Python</a>.</p><p>✅<a href="https://geekpython.in/template-inheritance-in-flask">Template inheritance in Flask</a>.</p><p>✅<a href="https://geekpython.in/type-hinting-in-python">How to use type hints in Python</a>?</p><p>✅<a href="https://geekpython.in/find-and-delete-mismatched-columns-from-dataframes-using-pandas">How to find and delete mismatched columns from datasets in pandas</a>?</p><p>✅<a href="https://geekpython.in/impact-of-learning-rates-on-ml-and-dl-models">How does the learning rate affect the ML and DL models</a>?</p><p><strong>That’s all for now.</strong></p><p><strong>Keep Coding✌✌.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3c44d4723290" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why Slash(/) and Asterisk(*) are Used in Functions?]]></title>
            <link>https://geekpython.medium.com/why-slash-and-asterisk-are-used-in-functions-f721a85bc36b?source=rss-98e733987320------2</link>
            <guid isPermaLink="false">https://medium.com/p/f721a85bc36b</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[function]]></category>
            <category><![CDATA[best-practices]]></category>
            <category><![CDATA[python3]]></category>
            <dc:creator><![CDATA[Sachin Pal]]></dc:creator>
            <pubDate>Mon, 04 Nov 2024 12:13:29 GMT</pubDate>
            <atom:updated>2024-11-04T12:13:29.543Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jNGLkYKMNWyrZA4cxy4AAA.png" /></figure><p>When you read the documentation of some functions you see a slash (/) and an asterisk (*) passed in the function definition. Why are they passed in a function?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GaDWPXCCk45XA4C6bqYodw.png" /></figure><h3>Why Slash and Asterisk are Used?</h3><p>The slash (/) and asterisk (*) are used to determine how an argument should pass when a function is called.</p><p>Simply said, the <strong>parameters on the left side of the slash (</strong>/<strong>) must be supplied as positional-only arguments</strong>, whereas the parameters on the right side can be passed as either positional or keyword arguments.</p><p><strong>An asterisk (</strong>*<strong>) indicates that the parameters on the right side must be supplied as keyword-only arguments</strong>, while the parameters on the left side can be passed as positional or keyword arguments.</p><pre>def func(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):<br>      -----------    ----------     ----------<br>        |             |                  |<br>        |        Positional or keyword   |<br>        |                                - Keyword only<br>         -- Positional only</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/803/1*2jMMMO-m8kCJ0f1jJ2M4HQ.png" /></figure><h3>Slash and Asterisk in Function Parameter</h3><p>Here’s a simple example to show that when slashes and asterisks appear in a function definition, then parameters are bound to positional-only and keyword-only arguments, respectively.</p><pre># Normal Function<br>def func_params(x, y, /, *, z):<br>    print(x, y, z)</pre><p>You can conclude that x and y are positional-only parameters, while z is keyword-only. Let&#39;s experiment to see if the parameters are bound to positional-only and keyword-only arguments.</p><pre>params = func_params(2, y=3, z=4)</pre><p>The parameter x is supplied as a positional argument, while y and z are passed as keywords or named arguments. When you run the code, the following output will be produced.</p><pre>Traceback (most recent call last):<br>  ...<br>    params = func_params(2, y=3, z=4)<br>             ^^^^^^^^^^^^^^^^^^^^^^^^<br>TypeError: func_params() got some positional-only arguments passed as keyword arguments: &#39;y&#39;</pre><p>Python raises a TypeError indicating that the positional-only argument (y) was given as a keyword argument. If you simply supply y as a positional argument, the code will not generate any errors.</p><pre># Normal Function<br>def func_params(x, y, /, *, z):<br>    print(x, y, z)<br><br># x &amp; y passed as positional argument and z as a keyword argument<br>params = func_params(2, 3, z=4)<br><br>--------------------<br>2 3 4</pre><h3>Can Asterisk Be Used Ahead of Slash</h3><p>The asterisk (*) must come after the slash (/). Now, you may be wondering if this is a rule.</p><p>You may think of it as a rule, if you use both a slash and an asterisk, the slash must come before the asterisk, otherwise, Python will throw a syntax error.</p><pre># Used asterisk before slash<br>def func_params(x, y, *, /, z):<br>    print(x, y, z)<br><br>params = func_params(2, 3, z=4)</pre><p>The code has been modified, and the asterisk is used before the slash. When you run the code, you’ll get a syntax error.</p><pre>...<br>    def func_params(x, y, *, /, z):<br>                             ^<br>SyntaxError: / must be ahead of *</pre><p>Logically inserting an asterisk before the slash doesn’t make sense because the parameters on the right side of the asterisk are keyword-only, but the parameters on the left side of the slash are positional-only.</p><p>This will not work at all. Here is an example to demonstrate this situation.</p><pre># Used asterisk before slash<br>def func_params(x, y, *, a, b, /, z):<br>    print(x, y, a, b, z)<br><br>params = func_params(2, 3, a=5, b=6, z=4)</pre><p>The parameters a and b are between the asterisk and the slash, making it unclear if they are positional or keyword-only parameters. This code will still throw a syntax error even after passing a and b as keyword arguments.</p><h3>Using Either One of Slash (/) or Asterisk (*) in the Function’s Parameter?</h3><p>You can utilise either one depending on the type of parameters you require, whether positional-only or keyword-only. Here’s an example showing the use of only a slash (/) in a function definition.</p><pre>def func_params(pos1, pos2, /, pos_or_kw):<br>    print(pos1, pos2, pos_or_kw)<br><br>params = func_params(2, 3, pos_or_kw=4)<br><br>--------------------<br>2 3 4</pre><p>The parameters pos1 and pos2 are positional-only parameters and pos_or_kw is a positional-or-keyword parameter.</p><p>Similarly, you can use a bare asterisk (*) also in the function definition.</p><pre>def func_params(pos_or_kw, *, kw1, kw2):<br>    print(pos_or_kw, kw1, kw2)<br><br>params = func_params(4, kw1=3, kw2=2)<br><br>--------------------<br>4 3 2</pre><h3>Writing a Function that Accepts Positional-only Arguments</h3><p>If you want to create a function or class that only accepts positional arguments, add the slash (/) at the end of the parameter list.</p><pre># Function that takes only positional arguments<br>def prescription(med1, med2, med3, /):<br>    print(&quot;Prescribed Meds:&quot;)<br>    print(f&quot;Med 1: {med1}&quot;)<br>    print(f&quot;Med 2: {med2}&quot;)<br>    print(f&quot;Med 3: {med3}&quot;)<br><br>prescription(&quot;Paracetamol&quot;, &quot;Omeprazole&quot;, &quot;Ibuprofen&quot;)<br><br>--------------------<br>Prescribed Meds:<br>Med 1: Paracetamol<br>Med 2: Omeprazole<br>Med 3: Ibuprofen</pre><p>The function prescription() takes three arguments: med1, med2, and med3. You can see that a slash (/) is written at the end of the parameters which makes them positional-only.</p><p>You’ll get an error if you try to pass a keyword argument to the function prescription().</p><pre>prescription(&quot;Paracetamol&quot;, &quot;Omeprazole&quot;, med3=&quot;Ibuprofen&quot;)<br>--------------------<br>Traceback (most recent call last):<br>  ...<br>    prescription(&quot;Paracetamol&quot;, &quot;Omeprazole&quot;, med3=&quot;Ibuprofen&quot;)<br>TypeError: prescription() got some positional-only arguments passed as keyword arguments: &#39;med3&#39;</pre><h3>Writing a Function that Accepts Keyword-only Arguments</h3><p>You can use a bare asterisk (*) in a function definition to make the parameters on the right side keyword-only.</p><p>If you want a function that only accepts keyword parameters, simply include a bare asterisk (*) at the beginning of the function parameter list.</p><pre># Function that takes only keyword arguments<br>def prescription(*, med1, med2, med3):<br>    print(&quot;Prescribed Meds:&quot;)<br>    print(f&quot;Med 1: {med1}&quot;)<br>    print(f&quot;Med 2: {med2}&quot;)<br>    print(f&quot;Med 3: {med3}&quot;)<br><br>prescription(med1=&quot;Paracetamol&quot;, med2=&quot;Omeprazole&quot;, med3=&quot;Ibuprofen&quot;)<br><br>-------------------<br>Prescribed Meds:<br>Med 1: Paracetamol<br>Med 2: Omeprazole<br>Med 3: Ibuprofen</pre><p>The function prescription() now only accepts keyword arguments and disallows positional arguments.</p><h3>Valid &amp; Invalid Function Definitions</h3><p>Here are some valid function definitions that can go well with slash and asterisk. <a href="https://peps.python.org/pep-0570/#specification">Source</a></p><pre>def f(p1, p2, /, p_or_kw, *, kw):<br>def f(p1, p2=None, /, p_or_kw=None, *, kw):<br>def f(p1, p2=None, /, *, kw):<br>def f(p1, p2=None, /):<br>def f(p1, p2, /, p_or_kw):<br>def f(p1, p2, /):</pre><p>You are most likely familiar with the rules for defining positional and keyword parameters, and the function definitions mentioned above are valid in accordance with those rules.</p><p>However, the following function definitions are considered invalid because they aren’t defined as per the rules.</p><pre>def f(p1, p2=None, /, p_or_kw, *, kw):<br>def f(p1=None, p2, /, p_or_kw=None, *, kw):<br>def f(p1=None, p2, /):</pre><p>Once the parameter with default value has been defined, all subsequent parameters (optional for parameters after the asterisk) must be defined with the default value to avoid ambiguity in function calls.</p><h3>Conclusion</h3><p>The slash (/) and asterisk (*) determine how an argument should be passed when you call the function.</p><p>A slash (/) in the function definition indicates that the parameters on the left side must be treated as positional-only.</p><p>A bare asterisk (*) in function definition indicates that the parameters on the right side must be treated as keyword-only.</p><p>You can use either both of them or a single in the function definition. When you are using both slash and asterisk, the slash must be inserted before the asterisk.</p><p>🏆<strong>Other articles you might be interested in if you liked this one</strong></p><p>✅<a href="https://geekpython.in/understanding-if-__name__-__main__-in-python-programs">Why if __name__ == “__main__” is used in Python programs</a>?</p><p>✅<a href="https://geekpython.in/yield-keyword-in-python">What is the yield keyword in Python</a>?</p><p>✅<a href="https://geekpython.in/build-websocket-server-and-client-using-python">Create a WebSocket server and client in Python</a>.</p><p>✅<a href="https://geekpython.in/decorators-in-python">How do decorators in Python work and how to create a custom decorator</a>?</p><p>✅<a href="https://geekpython.in/python-getitem-method">What is __getitem__ method in Python class</a>?</p><p>✅<a href="https://geekpython.in/map-function-in-python">The map() function in Python for mapping a function to each item in the iterable</a>?</p><p><strong>That’s all for now</strong></p><p><strong>Keep Coding✌✌</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f721a85bc36b" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>