<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: John Napiorkowski</title>
    <description>The latest articles on Forem by John Napiorkowski (@jjn1056).</description>
    <link>https://forem.com/jjn1056</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1119943%2F3492ab20-1496-43e6-a9b2-33f6a5adccae.jpeg</url>
      <title>Forem: John Napiorkowski</title>
      <link>https://forem.com/jjn1056</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jjn1056"/>
    <language>en</language>
    <item>
      <title>PAGI::Server, now with HTTP/2!</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Wed, 11 Feb 2026 19:05:39 +0000</pubDate>
      <link>https://forem.com/jjn1056/pagiserver-now-with-http2-37gf</link>
      <guid>https://forem.com/jjn1056/pagiserver-now-with-http2-37gf</guid>
      <description>&lt;p&gt;PAGI 0.001017 is on CPAN. The headline feature is HTTP/2 support in PAGI::Server, built on the nghttp2 C library and validated against h2spec's conformance suite. HTTP/2 is marked experimental in this release -- the protocol works, the compliance numbers are solid, and we want production feedback before dropping that label.&lt;/p&gt;

&lt;p&gt;This post focuses on why h2c (cleartext HTTP/2) between your reverse proxy and backend matters, and how PAGI::Server implements it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quick Version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the HTTP/2 dependency&lt;/span&gt;
cpanm Net::HTTP2::nghttp2

&lt;span class="c"&gt;# Run with HTTP/2 over TLS&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;--ssl-cert&lt;/span&gt; cert.pem &lt;span class="nt"&gt;--ssl-key&lt;/span&gt; key.pem app.pl

&lt;span class="c"&gt;# Run with cleartext HTTP/2 (h2c) -- behind a proxy&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app code doesn't change. HTTP/2 is a transport concern handled entirely by the server. The same async handlers that serve HTTP/1.1 serve HTTP/2 without modification.&lt;/p&gt;

&lt;h2&gt;
  
  
  "I Have nginx in Front, Why Do I Care?"
&lt;/h2&gt;

&lt;p&gt;If nginx terminates TLS and speaks HTTP/2 to clients, why does the backend need HTTP/2 too?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;h2c&lt;/strong&gt; -- cleartext HTTP/2 between your proxy and your application server. No TLS overhead, but all of HTTP/2's protocol benefits on the internal hop: stream multiplexing over a single TCP connection, HPACK header compression (especially effective for repetitive internal headers like auth tokens and tracing IDs), and per-stream flow control so a slow response on one stream doesn't block others.&lt;/p&gt;

&lt;p&gt;The practical wins: fewer TCP connections between proxy and backend (one multiplexed h2c connection replaces a pool of HTTP/1.1 connections), less file descriptor and kernel memory pressure, and no TIME_WAIT churn from connection recycling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where h2c Matters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt; requires HTTP/2 -- it doesn't work over HTTP/1.1 at all. If you're building gRPC services, h2c is mandatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API gateway fan-out&lt;/strong&gt; is where multiplexing shines. When your gateway fans out to 10 backend services per request, h2c means 1-2 connections per backend instead of a pool of 50-100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service mesh&lt;/strong&gt; environments (Envoy/Istio sidecars) default to HTTP/2 between services. A backend that speaks h2c natively means one less protocol translation.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Note on Proxies
&lt;/h3&gt;

&lt;p&gt;Not all proxies handle h2c equally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy&lt;/strong&gt; has the best h2c upstream support with full multiplexing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caddy&lt;/strong&gt; makes it trivial: &lt;code&gt;reverse_proxy h2c://localhost:8080&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx&lt;/strong&gt; supports h2c via &lt;code&gt;grpc_pass&lt;/code&gt; for gRPC workloads, but its generic &lt;code&gt;proxy_pass&lt;/code&gt; doesn't support &lt;code&gt;proxy_http_version 2.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For full multiplexing to backends, Envoy or Caddy are better choices than nginx today.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP/2 Over TLS -- No Proxy Required
&lt;/h2&gt;

&lt;p&gt;h2c isn't the only mode. PAGI::Server also does full HTTP/2 over TLS with ALPN negotiation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;--ssl-cert&lt;/span&gt; cert.pem &lt;span class="nt"&gt;--ssl-key&lt;/span&gt; key.pem app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when you don't want the overhead or complexity of a reverse proxy -- internal tools, admin dashboards, development servers, or any app where the traffic doesn't justify a separate proxy layer. Browsers get HTTP/2 directly, with TLS, no nginx required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PAGI::Server Does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dual-Mode Protocol Detection
&lt;/h3&gt;

&lt;p&gt;With TLS, PAGI::Server uses ALPN negotiation during the handshake -- advertising &lt;code&gt;h2&lt;/code&gt; and &lt;code&gt;http/1.1&lt;/code&gt;, letting the client choose. The protocol is decided before the first byte of application data.&lt;/p&gt;

&lt;p&gt;Without TLS (h2c mode), PAGI::Server inspects the first 24 bytes of each connection for the HTTP/2 client connection preface. If it matches, the connection upgrades to HTTP/2. If not, it falls through to HTTP/1.1. Both protocols coexist on the same port, same worker -- no configuration needed beyond &lt;code&gt;--http2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Either way, HTTP/1.1 clients are still served normally. The server handles both protocols on the same port.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket over HTTP/2 (RFC 8441)
&lt;/h3&gt;

&lt;p&gt;Most HTTP/2 implementations skip this. PAGI::Server supports the Extended CONNECT protocol from RFC 8441, which tunnels WebSocket connections over HTTP/2 streams. Multiple WebSocket connections multiplex over a single TCP connection instead of requiring one TCP connection each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compliance
&lt;/h3&gt;

&lt;p&gt;Built on nghttp2 (the same C library behind curl, Firefox, and Apache's mod_http2). PAGI::Server passes 137 of 146 h2spec conformance tests (93.8%). The 9 remaining failures are in nghttp2 itself and shared with every server that uses it. Load tested with h2load at 60,000 requests across 50 concurrent connections with no data loss or protocol violations.&lt;/p&gt;

&lt;p&gt;Full test-by-test results are published: &lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001017/view/lib/PAGI/Server/Compliance.pod" rel="noopener noreferrer"&gt;HTTP/2 Compliance Results&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Worker and Tunable
&lt;/h3&gt;

&lt;p&gt;HTTP/2 works in multi-worker prefork mode. Each worker independently handles HTTP/2 sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;--workers&lt;/span&gt; 4 app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protocol settings are exposed for environments that need fine-tuning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;app&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;http2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;h2_max_concurrent_streams&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# default: 100&lt;/span&gt;
    &lt;span class="s"&gt;h2_initial_window_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;131072&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# default: 65535&lt;/span&gt;
    &lt;span class="s"&gt;h2_max_frame_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# default: 16384&lt;/span&gt;
    &lt;span class="s"&gt;h2_max_header_list_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# default: 65536&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most deployments won't need to touch these. The defaults follow the RFC recommendations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context in the Perl Ecosystem
&lt;/h3&gt;

&lt;p&gt;Perl has had HTTP/2 libraries on CPAN (Protocol::HTTP2, Net::HTTP2), but application servers haven't integrated them with validated compliance testing. PAGI::Server is the first to publish h2spec results and ship h2c with automatic protocol detection alongside HTTP/1.1. If you're currently running Starman, Twiggy, or Hypnotoad, none of them offer HTTP/2.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Else Is in 0.001017
&lt;/h2&gt;

&lt;p&gt;The rest of the release is operational improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Worker heartbeat monitoring&lt;/strong&gt; -- parent process detects workers with blocked event loops and replaces them via SIGKILL + respawn. Default 50s timeout. Only triggers on true event loop starvation; async handlers using &lt;code&gt;await&lt;/code&gt; are unaffected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom access log format&lt;/strong&gt; -- format strings with atoms like &lt;code&gt;%a&lt;/code&gt; (address), &lt;code&gt;%s&lt;/code&gt; (status), &lt;code&gt;%D&lt;/code&gt; (duration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS performance fix&lt;/strong&gt; -- shared SSL context via &lt;code&gt;SSL_reuse_ctx&lt;/code&gt; eliminates per-connection CA bundle parsing. 26x throughput improvement at 8+ concurrent TLS connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE wire format fix&lt;/strong&gt; -- now handles CRLF, LF, and bare CR line endings per the SSE specification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-worker fixes&lt;/strong&gt; -- shutdown escalation, parameter pass-through, and various stability improvements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install PAGI&lt;/span&gt;
cpanm PAGI

&lt;span class="c"&gt;# Install HTTP/2 support (optional)&lt;/span&gt;
cpanm Net::HTTP2::nghttp2

&lt;span class="c"&gt;# Run your app with HTTP/2&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001017" rel="noopener noreferrer"&gt;PAGI 0.001017 on CPAN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001017/view/lib/PAGI/Server/Compliance.pod" rel="noopener noreferrer"&gt;HTTP/2 Compliance Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;PAGI on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/summerwind/h2spec" rel="noopener noreferrer"&gt;h2spec&lt;/a&gt; -- HTTP/2 conformance testing tool&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>perl</category>
      <category>webdev</category>
      <category>pagi</category>
    </item>
    <item>
      <title>PAGI 0.001016: Navigating the Future::IO Configuration Problem</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Mon, 02 Feb 2026 18:34:13 +0000</pubDate>
      <link>https://forem.com/jjn1056/pagi-0001016-navigating-the-futureio-configuration-problem-451d</link>
      <guid>https://forem.com/jjn1056/pagi-0001016-navigating-the-futureio-configuration-problem-451d</guid>
      <description>&lt;p&gt;PAGI 0.001016 includes a decision we wrestled with for a while: where should Future::IO configuration live? This post walks through the tradeoffs we considered and why we landed where we did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Future::IO?
&lt;/h2&gt;

&lt;p&gt;Perl's async ecosystem has multiple event loops: &lt;a href="https://metacpan.org/pod/IO::Async" rel="noopener noreferrer"&gt;IO::Async&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/Mojo::IOLoop" rel="noopener noreferrer"&gt;Mojo::IOLoop&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/AnyEvent" rel="noopener noreferrer"&gt;AnyEvent&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/UV" rel="noopener noreferrer"&gt;UV&lt;/a&gt;. This diversity is a strength - different loops have different strengths - but it creates a problem for library authors. If you write an async Redis client, which event loop do you target?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://metacpan.org/pod/Future::IO" rel="noopener noreferrer"&gt;Future::IO&lt;/a&gt; solves this by providing an abstraction layer. Libraries code against Future::IO's API (&lt;code&gt;sleep&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;), and Future::IO delegates to whatever event loop is actually running. This means libraries like &lt;a href="https://metacpan.org/pod/Async::Redis" rel="noopener noreferrer"&gt;Async::Redis&lt;/a&gt; work with &lt;em&gt;any&lt;/em&gt; event loop - IO::Async, UV, or anything else that has a Future::IO backend.&lt;/p&gt;

&lt;p&gt;The catch: Future::IO needs to be told which backend to use. It doesn't auto-detect the running loop. Someone has to explicitly configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;  &lt;span class="c1"&gt;# Tell Future::IO to use IO::Async&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration must happen before any Future::IO operations are attempted. Get it wrong - or forget it entirely - and things break in confusing ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Given that Future::IO needs explicit configuration, who should do it? Someone has to call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;  &lt;span class="c1"&gt;# or 'UV', etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question is: who?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Doesn't Work
&lt;/h2&gt;

&lt;p&gt;A quick note: PAGI is built on the async Perl ecosystem created largely by Paul Evans (LeoNerd) - &lt;a href="https://metacpan.org/pod/IO::Async" rel="noopener noreferrer"&gt;IO::Async&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/Future" rel="noopener noreferrer"&gt;Future&lt;/a&gt;, and &lt;a href="https://metacpan.org/pod/Future::IO" rel="noopener noreferrer"&gt;Future::IO&lt;/a&gt;. His guidance shaped our thinking here, and the constraints he identified are real engineering concerns, not arbitrary rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Libraries configuring Future::IO&lt;/strong&gt; - This was our first instinct. Async::Redis could call &lt;code&gt;Future::IO-&amp;gt;load_best_impl&lt;/code&gt; to auto-detect the best backend. But &lt;code&gt;load_best_impl&lt;/code&gt; picks based on what's &lt;em&gt;installed&lt;/em&gt;, not what's &lt;em&gt;running&lt;/em&gt;. If you have UV installed but you're running under IO::Async (like PAGI::Server), you get a mismatch. Things explode.&lt;/p&gt;

&lt;p&gt;Paul's guidance: libraries shouldn't configure Future::IO because they don't know the runtime context. This makes sense - a library is a guest in someone else's application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PAGI::Server (the module) configuring it&lt;/strong&gt; - We tried this too. It seemed logical - PAGI::Server runs IO::Async, so it knows the context. But there's a legitimate concern: PAGI::Server is still a module being &lt;code&gt;use&lt;/code&gt;d, and someone might embed it in a larger application.&lt;/p&gt;

&lt;p&gt;Consider: you're running PAGI::Server alongside other event-driven code - maybe a metrics collector, a background job processor, or a connection to a message queue. Your main script creates the IO::Async loop and adds multiple notifiers to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;IO::Async::&lt;/span&gt;&lt;span class="nv"&gt;Loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Your other async code&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$metrics_reporter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$job_processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;# PAGI::Server is just one component&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this scenario, the main script is the entry point, not PAGI::Server. If PAGI::Server configured Future::IO, it would be a module reaching out to modify global state - potentially conflicting with configuration the main script already set up. The principle that only entry points should configure global state exists precisely for cases like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tradeoffs We Considered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Apps Configure Future::IO
&lt;/h3&gt;

&lt;p&gt;The "pure" approach - make apps explicit about their dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.pl&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Async::&lt;/span&gt;&lt;span class="nv"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Explicit, no magic, apps declare what they need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; This fundamentally breaks PAGI's value proposition. The whole point of PAGI is write-once portability - your app should run under any PAGI-compliant server. If your app.pl says &lt;code&gt;load_impl('IOAsync')&lt;/code&gt;, what happens when you want to run under &lt;a href="https://metacpan.org/pod/Conduit" rel="noopener noreferrer"&gt;Conduit&lt;/a&gt; (Paul's Future::IO-native web server that currently supports PSGI and may support PAGI in the future)? You'd have to change your app code to switch servers. That's exactly what PAGI exists to prevent.&lt;/p&gt;

&lt;p&gt;I kept coming back to this. Boilerplate is annoying but tolerable. Breaking server-agnosticism is a dealbreaker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Extend the PAGI Spec
&lt;/h3&gt;

&lt;p&gt;We could add async capabilities directly to the PAGI spec - have servers provide event loop primitives through the scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Hypothetical: server provides async capabilities&lt;/span&gt;
&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{'&lt;/span&gt;&lt;span class="s1"&gt;pagi.sleep&lt;/span&gt;&lt;span class="p"&gt;'}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{'&lt;/span&gt;&lt;span class="s1"&gt;pagi.timeout&lt;/span&gt;&lt;span class="p"&gt;'}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Clean abstraction. Apps use spec-defined capabilities, servers implement them however they want. True portability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Where does it stop? PAGI is modeled after Python's ASGI, which is deliberately minimal - it defines the request/response lifecycle and nothing more. Once you add sleep, what about DNS resolution? Database connections? HTTP clients?&lt;/p&gt;

&lt;p&gt;The spec becomes a grab-bag of async primitives, and every PAGI server implementation has to support all of them. That's a maintenance burden that discourages new server implementations. ASGI's success comes partly from its simplicity - implementing a basic ASGI server is tractable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Entry Point Configures
&lt;/h3&gt;

&lt;p&gt;Have &lt;code&gt;pagi-server&lt;/code&gt; (the CLI script, not the module) configure Future::IO. The script &lt;em&gt;is&lt;/em&gt; the application entry point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Follows the "only entry points configure" principle. CLI users get zero-config experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Still feels like action at distance. Programmatic users of PAGI::Server must configure it themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Chose
&lt;/h2&gt;

&lt;p&gt;We went with Option 3. In &lt;code&gt;pagi-server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In PAGI::Runner (called by pagi-server)&lt;/span&gt;
&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;_configure_future_io&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$configured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="nn"&gt;Future::IO::Impl::&lt;/span&gt;&lt;span class="nv"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$configured&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="ow"&gt;ne&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;quiet&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;warn&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Future::IO configured for IO::Async&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pagi-server myapp.pl&lt;/code&gt;&lt;/strong&gt; - Future::IO auto-configured, Async::Redis and other Future::IO libraries just work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PAGI::Server-&amp;gt;new(...)&lt;/code&gt; programmatically&lt;/strong&gt; - You configure Future::IO yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For programmatic usage, here's what that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env perl&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;IO::Async::&lt;/span&gt;&lt;span class="nv"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Configure Future::IO BEFORE loading libraries that use it&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="c1"&gt;# Now these work correctly&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Async::&lt;/span&gt;&lt;span class="nv"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Async::&lt;/span&gt;&lt;span class="nv"&gt;Redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... your app using $redis&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;IO::Async::&lt;/span&gt;&lt;span class="nv"&gt;Loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;listen&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're comfortable with this tradeoff. If you're writing your own server orchestration - creating loops, adding notifiers, managing the lifecycle - you're already in "I know what I'm doing" territory. You're the kind of developer who reads documentation about event loop integration. Adding two lines to configure Future::IO is not a burden for someone already writing fifteen lines of loop setup.&lt;/p&gt;

&lt;p&gt;The CLI user who just wants &lt;code&gt;pagi-server app.pl&lt;/code&gt; to work shouldn't need to know any of this. The power user embedding PAGI::Server in a custom harness can handle it. (See the &lt;a href="https://metacpan.org/pod/PAGI::Server#LOOP-INTEROPERABILITY" rel="noopener noreferrer"&gt;LOOP INTEROPERABILITY&lt;/a&gt; section in PAGI::Server docs.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Felt Right (Eventually)
&lt;/h2&gt;

&lt;p&gt;I had doubts. Isn't this still "magic"? Isn't the app implicitly depending on server configuration?&lt;/p&gt;

&lt;p&gt;But here's the reframing that helped: &lt;strong&gt;configuring the async runtime is the server's job&lt;/strong&gt;. Python's ASGI servers don't ask users to "configure asyncio" - the runtime just works. The server provides the execution environment.&lt;/p&gt;

&lt;p&gt;The key insight is that this isn't about any single library. It's about the entire Future::IO ecosystem - database drivers, HTTP clients, Redis, message queues, and anything else built on Future::IO. These libraries represent the future of async Perl. If every PAGI user has to understand Future::IO configuration internals before using any of them, that's a barrier to adoption that hurts the whole ecosystem.&lt;/p&gt;

&lt;p&gt;By having &lt;code&gt;pagi-server&lt;/code&gt; configure Future::IO, we're saying: "When you run under pagi-server, Future::IO libraries work. That's part of what the server provides." It's similar to how Python's ASGI servers provide an asyncio environment - you don't configure the async runtime, you just use libraries that depend on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in This Release: SSE Query Parameters
&lt;/h2&gt;

&lt;p&gt;PAGI::SSE now has query parameter parsing, matching PAGI::Request and PAGI::WebSocket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$sse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;SSE&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sse&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;query_param&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sse&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# Hash::MultiValue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is straightforward consistency work - all three scope types now have the same query parameter API.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Experimental - RFC Welcome
&lt;/h2&gt;

&lt;p&gt;I want to be clear: &lt;strong&gt;the Future::IO integration in this release is experimental&lt;/strong&gt;. We've made a pragmatic choice that works, but I'm not certain it's the right long-term answer.&lt;/p&gt;

&lt;p&gt;The PAGI spec will probably need to address async capability configuration at some point. I'm just not sure what that should look like yet. Should servers declare what async backend they use? Should there be a standard way for apps to discover capabilities? Should Future::IO configuration be part of the lifespan protocol?&lt;/p&gt;

&lt;p&gt;I'm treating this release as a request for comments. If you have thoughts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether &lt;code&gt;pagi-server&lt;/code&gt; auto-configuring Future::IO is the right call&lt;/li&gt;
&lt;li&gt;How the PAGI spec should handle async ecosystem integration&lt;/li&gt;
&lt;li&gt;Alternative approaches we haven't considered&lt;/li&gt;
&lt;li&gt;Pain points you hit when using Future::IO libraries with PAGI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...I want to hear them. Please leave your thoughts on &lt;a href="https://github.com/jjn1056/pagi/issues/33" rel="noopener noreferrer"&gt;GitHub issue #33&lt;/a&gt;, which I've opened specifically for this discussion.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PAGI is a PSGI-like specification for async Perl web applications. Feedback welcome at &lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;github.com/jjn1056/pagi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devjournal</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>The #1 sentence I add to prompts that makes them better</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 25 Jan 2026 00:16:51 +0000</pubDate>
      <link>https://forem.com/jjn1056/the-1-sentence-i-add-to-prompts-that-makes-them-better-514n</link>
      <guid>https://forem.com/jjn1056/the-1-sentence-i-add-to-prompts-that-makes-them-better-514n</guid>
      <description>&lt;h2&gt;
  
  
  Prompt Engineering or selling your soul?
&lt;/h2&gt;

&lt;p&gt;If you use AI at work, you’ve probably had this experience:&lt;/p&gt;

&lt;p&gt;You write a prompt that feels &lt;em&gt;clear enough&lt;/em&gt;, hit enter, and the model confidently produces something that is… technically fine… but not what you meant.&lt;/p&gt;

&lt;p&gt;Maybe it built the right thing in the wrong style.&lt;br&gt;
Maybe it chose an approach you would never ship.&lt;br&gt;
Maybe it made assumptions you didn’t realize you were leaving unstated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’ve started treating prompts like a contract with the devil.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because I think AI is evil — just because it’s &lt;em&gt;literal&lt;/em&gt;, opportunistic, and perfectly willing to sprint in the wrong direction if you give it even a small opening.&lt;/p&gt;

&lt;p&gt;And you can’t cover every edge case up front.&lt;/p&gt;

&lt;p&gt;So here’s the one sentence I add to a lot of my prompts that consistently makes the results better:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before you begin, ask any clarifying questions you need to fully understand what I’m asking and to do an excellent job.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why it works (and why it’s “vibe engineering”)
&lt;/h2&gt;

&lt;p&gt;Most prompting advice is basically: &lt;em&gt;be more specific.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s true, but it’s incomplete — because the whole problem is that you often &lt;strong&gt;don’t realize what you forgot to specify&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This sentence flips the dynamic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of &lt;em&gt;“I describe something and hope the AI guesses right,”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;it becomes &lt;em&gt;collaborative.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It turns the model into a reviewer &lt;strong&gt;before&lt;/strong&gt; it becomes an implementer.&lt;/p&gt;

&lt;p&gt;And it forces the “unknown unknowns” to show up early, while it’s still cheap to correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kind of questions that save you
&lt;/h2&gt;

&lt;p&gt;My favorite clarifying questions are the ones that expose missing context I didn’t realize mattered.&lt;/p&gt;

&lt;p&gt;Like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Is there an existing system you want me to use as a template?”&lt;/li&gt;
&lt;li&gt;“Is this a large table / high-traffic database?”&lt;/li&gt;
&lt;li&gt;“Is this safe to run during business hours?”&lt;/li&gt;
&lt;li&gt;“What does success look like: correctness, speed, low risk, or minimal code changes?”&lt;/li&gt;
&lt;li&gt;“Do you care about test coverage, or just a working fix?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions aren’t just helpful to the AI.&lt;/p&gt;

&lt;p&gt;They’re helpful to &lt;em&gt;me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because half the time, I’m using the AI to tease out details I forgot to include in the prompt in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic failure mode this prevents
&lt;/h2&gt;

&lt;p&gt;Say you ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Write a migration to backfill X safely.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI might happily generate a perfectly valid migration that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locks the table,&lt;/li&gt;
&lt;li&gt;runs as one big transaction,&lt;/li&gt;
&lt;li&gt;does a full scan,&lt;/li&gt;
&lt;li&gt;adds an index in a way that takes forever,&lt;/li&gt;
&lt;li&gt;and generally assumes the world is a small quiet sandbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not “wrong” — it’s just not something you want to discover &lt;em&gt;after&lt;/em&gt; you already committed to the approach.&lt;/p&gt;

&lt;p&gt;If you make the model interview you first, you’ll often get the question you forgot to say out loud:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How large is the table, and can this run on prod without blocking writes?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one question can save you a very annoying afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I &lt;em&gt;don’t&lt;/em&gt; use it
&lt;/h2&gt;

&lt;p&gt;I don’t paste this sentence into every single prompt.&lt;/p&gt;

&lt;p&gt;If I’m asking something small and obvious (“write this one-liner”, “explain this error”, “rename these variables”), it’s unnecessary overhead.&lt;/p&gt;

&lt;p&gt;But if I’m doing any of these, it comes out almost automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writing something new (a real feature, not a snippet)&lt;/li&gt;
&lt;li&gt;debugging a complex problem&lt;/li&gt;
&lt;li&gt;anything with multiple moving parts&lt;/li&gt;
&lt;li&gt;anything where “technically correct” can still waste time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copy/paste template you can steal
&lt;/h2&gt;

&lt;p&gt;Here’s a version you can drop into your own prompts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My default prompt preamble&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before you begin, ask any clarifying questions you need to fully understand what I’m asking and to do an excellent job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Optional follow-up (if you want to be extra explicit)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If something is ambiguous, &lt;strong&gt;don’t guess&lt;/strong&gt; — ask.&lt;/li&gt;
&lt;li&gt;If there are multiple valid approaches, &lt;strong&gt;list the options&lt;/strong&gt; and tell me what you recommend and why.&lt;/li&gt;
&lt;li&gt;After questions are answered, produce the output in a clean, usable format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;It’s not magic. It doesn’t make the model smarter.&lt;/p&gt;

&lt;p&gt;It just keeps the process collaborative instead of “write a prompt and hope for the best.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teaser:&lt;/strong&gt; if you want to hear about the &lt;strong&gt;#2 sentence I add to prompts&lt;/strong&gt; to make them even better, get me to &lt;strong&gt;100 likes&lt;/strong&gt; 😄&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>promptengineering</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>PAGI::Server: Stress-Testing an Async Perl HTTP Server the Hard Way</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Thu, 01 Jan 2026 21:52:45 +0000</pubDate>
      <link>https://forem.com/jjn1056/pagiserver-stress-testing-an-async-perl-http-server-the-hard-way-i2e</link>
      <guid>https://forem.com/jjn1056/pagiserver-stress-testing-an-async-perl-http-server-the-hard-way-i2e</guid>
      <description>&lt;p&gt;Over the last few days I’ve been stress-testing &lt;strong&gt;PAGI::Server&lt;/strong&gt;, an async HTTP server built on &lt;strong&gt;IO::Async&lt;/strong&gt;, &lt;strong&gt;Future::IO&lt;/strong&gt;, and an &lt;strong&gt;EV&lt;/strong&gt; event loop. The goal wasn’t to chase synthetic “requests per second” numbers, but to answer harder questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does streaming actually behave correctly?&lt;/li&gt;
&lt;li&gt;What happens under sustained concurrency?&lt;/li&gt;
&lt;li&gt;Do slow clients cause buffer bloat?&lt;/li&gt;
&lt;li&gt;Does backpressure really work?&lt;/li&gt;
&lt;li&gt;How does it fail under overload?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post summarizes what I learned by pushing the server well past “normal” workloads and watching it misbehave — or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one paragraph
&lt;/h2&gt;

&lt;p&gt;PAGI::Server is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single-process, event-driven by default&lt;/li&gt;
&lt;li&gt;optionally multi-process via workers&lt;/li&gt;
&lt;li&gt;fully async end-to-end (no threads)&lt;/li&gt;
&lt;li&gt;streaming-first (responses are not buffered by default)&lt;/li&gt;
&lt;li&gt;designed to support ASGI-like semantics in Perl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server accepts connections in a main event loop and routes work through &lt;strong&gt;IO::Async::Stream&lt;/strong&gt; objects. Application code produces responses via a &lt;code&gt;$send&lt;/code&gt; callback that emits protocol events (&lt;code&gt;http.response.start&lt;/code&gt;, &lt;code&gt;http.response.body&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;p&gt;That last part matters, because it’s where most async servers quietly cheat.&lt;/p&gt;

&lt;h2&gt;
  
  
  An important note: Although PAGI::Server is IO::Async based, that is an implementation detail, not a core requirement of the PAGI specification.  That said I build PAGI::Server on IO::Async because it's mature, battle tested and fully featured.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Baseline: fast responses
&lt;/h2&gt;

&lt;p&gt;Before touching streaming, I tested a trivial HTTP handler.&lt;/p&gt;

&lt;p&gt;On an older 16-core MacBook Pro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~22–23k requests/sec&lt;/li&gt;
&lt;li&gt;p50 latency ~20ms&lt;/li&gt;
&lt;li&gt;p99 &amp;lt; 50ms&lt;/li&gt;
&lt;li&gt;CPU mostly idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing surprising here — just confirming there’s no accidental serialization or blocking in the accept loop.&lt;/p&gt;

&lt;p&gt;Just to set some baselines, I tested a simple, similar PSGI application using Starman, one of the most popular choices for serving PSGI apps, and I found it topped out at around 16K requests/sec, with significantly worse latency spread and dropped connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Streaming test: 2-second responses, 500 concurrent clients
&lt;/h2&gt;

&lt;p&gt;Next, I switched to a streaming handler that sends chunks over ~2 seconds.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;500 concurrent connections&lt;/strong&gt;, single process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~242–245 requests/sec&lt;/li&gt;
&lt;li&gt;p50 ≈ 2.00s&lt;/li&gt;
&lt;li&gt;p99 ≈ 2.3–2.4s&lt;/li&gt;
&lt;li&gt;CPU ~10–20% busy&lt;/li&gt;
&lt;li&gt;memory stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because the math checks out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 concurrent / 2 seconds ≈ 250 rps theoretical max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hitting ~97–98% of theoretical max strongly suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no head-of-line blocking&lt;/li&gt;
&lt;li&gt;no per-connection threads&lt;/li&gt;
&lt;li&gt;no accidental buffering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server is doing what an async server should do: waiting, not working.&lt;/p&gt;




&lt;h2&gt;
  
  
  A gotcha: TTY logging will ruin your benchmark
&lt;/h2&gt;

&lt;p&gt;One early run looked worse than expected. The cause was embarrassingly simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Access logging was printing to a terminal (TTY).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At ~20k+ log lines/sec, the terminal became the bottleneck.&lt;/p&gt;

&lt;p&gt;Once logging was disabled or redirected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage dropped dramatically&lt;/li&gt;
&lt;li&gt;latency tails tightened&lt;/li&gt;
&lt;li&gt;throughput returned to theoretical limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: &lt;strong&gt;never benchmark with synchronous TTY logging enabled&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Overload behavior: what happens at 2500 concurrent streams?
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;ulimit -n = 2048&lt;/code&gt;, single-process PAGI::Server started rejecting connections around ~2000 open sockets.&lt;/p&gt;

&lt;p&gt;That rejection path returned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Retry-After: 5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection: close&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;good behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of accepting everything and melting down, the server applied &lt;em&gt;admission control&lt;/em&gt; based on available file descriptors.&lt;/p&gt;

&lt;p&gt;The client (&lt;code&gt;hey&lt;/code&gt;) complained with “unsolicited response” warnings — a known artifact when clients aggressively reuse keep-alive connections under churn. The server was behaving correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaling out: 16 workers
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;16 worker processes&lt;/strong&gt;, the same workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sustained ~1200 requests/sec at 2500 concurrency&lt;/li&gt;
&lt;li&gt;p50 ≈ 2.00s&lt;/li&gt;
&lt;li&gt;p99 ≈ 3.0s&lt;/li&gt;
&lt;li&gt;all 200 responses, no rejects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, the math checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2500 concurrent / 2 seconds ≈ 1250 rps theoretical
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: ~96% of theoretical max.&lt;/p&gt;

&lt;p&gt;This validated that PAGI’s worker model increases capacity cleanly without breaking streaming semantics.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real test: slow clients and backpressure
&lt;/h2&gt;

&lt;p&gt;Throughput benchmarks are easy. &lt;strong&gt;Backpressure is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To test this, I used a stress app that streams tens of megabytes as fast as possible, then ran clients with artificial throttling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow client test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--limit-rate&lt;/span&gt; 1M http://localhost:5000/stream/50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~50MB transferred in ~49 seconds&lt;/li&gt;
&lt;li&gt;download rate stayed near 1MB/s&lt;/li&gt;
&lt;li&gt;server did &lt;strong&gt;not&lt;/strong&gt; buffer the entire response&lt;/li&gt;
&lt;li&gt;no memory blow-up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the critical signal.&lt;/p&gt;

&lt;p&gt;If backpressure were broken, the server would buffer 50MB instantly and curl would “catch up” later. That did not happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-throughput clients
&lt;/h2&gt;

&lt;p&gt;Unthrottled clients on localhost achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~75–80 MB/s per connection&lt;/li&gt;
&lt;li&gt;linear scaling until loopback bandwidth became the limit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, no red flags.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concurrency + large bodies
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;hey&lt;/code&gt; with 50 concurrent clients streaming 10–50MB bodies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high aggregate throughput&lt;/li&gt;
&lt;li&gt;wide latency distribution&lt;/li&gt;
&lt;li&gt;no error rates&lt;/li&gt;
&lt;li&gt;no collapse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the bottleneck was clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kernel socket buffers&lt;/li&gt;
&lt;li&gt;memory copy bandwidth&lt;/li&gt;
&lt;li&gt;client tooling limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not the server itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The subtle risk we identified (and addressed)
&lt;/h2&gt;

&lt;p&gt;While reviewing the server code, one important detail surfaced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writes use &lt;code&gt;IO::Async::Stream-&amp;gt;write&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;writes enqueue data into an outgoing buffer&lt;/li&gt;
&lt;li&gt;there is no implicit send-side backpressure unless implemented explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is fine for polite producers (like most apps), but dangerous if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an app writes huge chunks rapidly&lt;/li&gt;
&lt;li&gt;the client reads slowly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To address this, we added &lt;strong&gt;send-side backpressure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;track queued outgoing bytes&lt;/li&gt;
&lt;li&gt;define high / low watermarks&lt;/li&gt;
&lt;li&gt;pause &lt;code&gt;$send-&amp;gt;(...)&lt;/code&gt; futures when buffers exceed the high watermark&lt;/li&gt;
&lt;li&gt;resume when the buffer drains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This change makes slow-client behavior &lt;em&gt;safe by default&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I did &lt;em&gt;not&lt;/em&gt; see (important)
&lt;/h2&gt;

&lt;p&gt;Across all tests, I did &lt;strong&gt;not&lt;/strong&gt; observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unbounded RSS growth&lt;/li&gt;
&lt;li&gt;runaway CPU&lt;/li&gt;
&lt;li&gt;increasing latency under steady load&lt;/li&gt;
&lt;li&gt;dropped connections without errors&lt;/li&gt;
&lt;li&gt;event-loop starvation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the usual failure modes of async servers. None appeared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final assessment
&lt;/h2&gt;

&lt;p&gt;Based on these tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PAGI::Server handles streaming correctly&lt;/li&gt;
&lt;li&gt;backpressure works (and is now explicit)&lt;/li&gt;
&lt;li&gt;overload fails fast and cleanly&lt;/li&gt;
&lt;li&gt;worker scaling behaves predictably&lt;/li&gt;
&lt;li&gt;performance matches theoretical limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s still more to do — especially long-duration soak tests and production-grade observability — but the fundamentals are solid.&lt;/p&gt;

&lt;p&gt;Most importantly, &lt;strong&gt;the server behaves honestly&lt;/strong&gt;. It doesn’t fake async by buffering everything and hoping for the best.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>testing</category>
      <category>performance</category>
    </item>
    <item>
      <title>Perl PAGI Project Update</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 28 Dec 2025 17:14:55 +0000</pubDate>
      <link>https://forem.com/jjn1056/perl-pagi-project-update-2n2p</link>
      <guid>https://forem.com/jjn1056/perl-pagi-project-update-2n2p</guid>
      <description>&lt;h1&gt;
  
  
  What We Learned Shipping PAGI
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001011" rel="noopener noreferrer"&gt;PAGI&lt;/a&gt; (Perl Asynchronous Gateway Interface) is a new web specification and reference server for Perl, designed to bring first-class async/await support to web development. Think of it as Perl's answer to Python's ASGI - a modern foundation for WebSocket, Server-Sent Events, and HTTP applications using &lt;a href="https://metacpan.org/pod/Future" rel="noopener noreferrer"&gt;Future&lt;/a&gt; and &lt;a href="https://metacpan.org/pod/Future::AsyncAwait" rel="noopener noreferrer"&gt;Future::AsyncAwait&lt;/a&gt; syntax.&lt;/p&gt;

&lt;p&gt;Since the first stable release on December 24th, we've shipped seven releases in four days. This pace wasn't planned - it emerged from squashing bugs reported by &lt;a href="http://www.cpantesters.org/" rel="noopener noreferrer"&gt;CPAN Testers&lt;/a&gt;, especially on important but less common platforms like FreeBSD, along with rapid iteration on the API. Here's what we learned along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the Right Code is No Code
&lt;/h2&gt;

&lt;p&gt;One of the more interesting decisions we made was &lt;em&gt;removing&lt;/em&gt; a feature: built-in sendfile support.&lt;/p&gt;

&lt;p&gt;The initial implementation used &lt;a href="https://metacpan.org/pod/IO::Async::FileStream" rel="noopener noreferrer"&gt;IO::Async::FileStream&lt;/a&gt; with &lt;a href="https://metacpan.org/pod/Sys::Sendfile" rel="noopener noreferrer"&gt;Sys::Sendfile&lt;/a&gt; to efficiently stream large files directly from disk to socket, bypassing userspace copying. It worked beautifully on Linux. Then we tested on FreeBSD.&lt;/p&gt;

&lt;p&gt;The problem wasn't just FreeBSD - it was the interaction between sendfile and non-blocking sockets, edge cases with partial writes, and the complexity of handling all the ways different operating systems implement (or don't implement) zero-copy file transfers. We found ourselves writing increasingly elaborate platform-specific workarounds.&lt;/p&gt;

&lt;p&gt;We stepped back and asked: what problem are we actually solving? In production, large file serving should go through a reverse proxy anyway. Nginx's X-Sendfile (or X-Accel-Redirect) and Apache's mod_xsendfile exist precisely for this use case. They're battle-tested, optimized, and someone else maintains them.&lt;/p&gt;

&lt;p&gt;So we removed sendfile entirely and created &lt;a href="https://metacpan.org/pod/PAGI::Middleware::XSendfile" rel="noopener noreferrer"&gt;PAGI::Middleware::XSendfile&lt;/a&gt; instead. Your PAGI application returns a special header, and your reverse proxy handles the actual file transfer. The Unix philosophy wins again: do one thing well, and compose with specialized tools.&lt;/p&gt;

&lt;p&gt;That said, this isn't ideological. If someone wants to contribute a robust, cross-platform sendfile implementation, we'd welcome the PR. The current solution is pragmatic, not dogmatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Error Handling Done Right
&lt;/h2&gt;

&lt;p&gt;Asynchronous programming has a dirty secret: errors love to disappear.&lt;/p&gt;

&lt;p&gt;In synchronous code, an exception bubbles up naturally. In async code with fire-and-forget Futures, exceptions can vanish into the void. Your background task fails, nobody notices, and you spend hours wondering why something silently stopped working.&lt;/p&gt;

&lt;p&gt;PAGI originally used a pattern called &lt;code&gt;retain()&lt;/code&gt; to keep Futures alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;retain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;some_async_operation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked for keeping the Future from being garbage collected, but it had a problem: if &lt;code&gt;some_async_operation()&lt;/code&gt; failed, the error was quietly swallowed. The Future failed, nobody was listening, and life went on - except for the bug you didn't know about.&lt;/p&gt;

&lt;p&gt;We replaced this with &lt;code&gt;adopt_future()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;adopt_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;some_async_operation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is crucial. When an adopted Future fails, the error propagates to the parent context and gets logged. You see it. You can debug it. The failure is observable.&lt;/p&gt;

&lt;p&gt;This applies throughout PAGI's internals - connection handling, protocol parsing, worker lifecycle management. Errors that previously might have disappeared now surface properly. It's a small API change that dramatically improves debuggability.&lt;/p&gt;

&lt;p&gt;For application developers, the pattern extends to your own code. When you spawn background work in a request handler, adopt it rather than retaining it. Your future self debugging a production issue will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making PAGI Feel Like Perl
&lt;/h2&gt;

&lt;p&gt;One principle guided several of our developer experience decisions: PAGI should feel like Perl, not like a foreign framework that happens to run on Perl.&lt;/p&gt;

&lt;p&gt;The clearest example is the new &lt;code&gt;-e&lt;/code&gt; and &lt;code&gt;-M&lt;/code&gt; flags for &lt;code&gt;pagi-server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inline app, just like perl -e&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'sub { my ($scope, $receive, $send) = @_; ... }'&lt;/span&gt;

&lt;span class="c"&gt;# Load modules first, like perl -M&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;-MPAGI&lt;/span&gt;::App::File &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'PAGI::App::File-&amp;gt;new(root =&amp;gt; ".")-&amp;gt;to_app'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've used &lt;code&gt;perl -e&lt;/code&gt; for quick scripts, this syntax is immediately familiar. No ceremony, no boilerplate files for simple cases.&lt;/p&gt;

&lt;p&gt;This philosophy extends to other areas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test::Client&lt;/strong&gt; now traps exceptions by default, matching how Perl developers expect test failures to behave. Multi-value headers, query parameters, and form fields work naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment modes&lt;/strong&gt; follow the pattern established by &lt;a href="https://metacpan.org/pod/Plack" rel="noopener noreferrer"&gt;Plack&lt;/a&gt;: &lt;code&gt;-E development&lt;/code&gt; enables debugging middleware, &lt;code&gt;-E production&lt;/code&gt; optimizes for performance. The detection is automatic based on TTY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal handling&lt;/strong&gt; follows Unix conventions. SIGTERM and SIGINT trigger graceful shutdown. SIGHUP reloads workers. SIGTTIN/SIGTTOU adjust worker counts. It behaves like the system tools Perl developers already know.&lt;/p&gt;

&lt;p&gt;The goal is reducing cognitive overhead. You shouldn't need to learn "the PAGI way" for things Perl and Unix already have conventions for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;PAGI is stabilizing, but there's plenty of interesting work ahead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation has been a major focus.&lt;/strong&gt; We split the original Tutorial into two documents: a &lt;a href="https://metacpan.org/pod/PAGI::Tutorial" rel="noopener noreferrer"&gt;Tutorial&lt;/a&gt; for getting started with core concepts, and a &lt;a href="https://metacpan.org/pod/PAGI::Cookbook" rel="noopener noreferrer"&gt;Cookbook&lt;/a&gt; for advanced patterns like background tasks, database connection pooling, JWT authentication, and Redis-backed sessions. The goal is progressive disclosure - you shouldn't need to read about WebSocket heartbeat strategies before you've served your first HTTP response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The middleware collection&lt;/strong&gt; continues to grow. We have 37 middleware components now (authentication, rate limiting, CORS, sessions, and more), but there's always room for more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're considering a repository split&lt;/strong&gt; and would love community feedback. Currently PAGI ships everything in one distribution: core spec, reference server, 37 middleware components, 19 apps, and test utilities. This is convenient (&lt;code&gt;cpanm PAGI&lt;/code&gt; gets you everything) but means installing the full stack even if you only need parts.&lt;/p&gt;

&lt;p&gt;Options we're weighing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep unified&lt;/strong&gt; (current) - simpler while the spec is still evolving&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split server&lt;/strong&gt; - separate PAGI::Server as "reference implementation," enabling alternative servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three-way split&lt;/strong&gt; - core spec, server, and contrib (middleware/apps) as separate distributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would serve you better? Lighter installs? Easier contribution workflow? Alternative server implementations? We'd appreciate hearing your use case.&lt;/p&gt;

&lt;p&gt;Most importantly, we're looking for contributors. The codebase is approachable - modern Perl with async/await, comprehensive tests, and documented internals. Whether you want to tackle something meaty like cross-platform sendfile, or something focused like a new middleware component, there's room for your ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;github.com/jjn1056/pagi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPAN:&lt;/strong&gt; &lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001011" rel="noopener noreferrer"&gt;PAGI on MetaCPAN&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jjn1056/pagi/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;:&lt;/strong&gt; Guidelines for contributors, including our approach to AI-assisted development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jjn1056/pagi/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;:&lt;/strong&gt; How to report vulnerabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PAGI is licensed under the Artistic License 2.0. It's a volunteer project - no timeline commitments, but serious about quality. If your organization needs priority support or custom development, contract work is available.&lt;/p&gt;

&lt;p&gt;Async Perl web development is here. Let's build it together.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>perl</category>
    </item>
    <item>
      <title>PAGI::Server Performance and Hardening</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Mon, 15 Dec 2025 16:52:22 +0000</pubDate>
      <link>https://forem.com/jjn1056/pagiserver-performance-and-hardening-l8d</link>
      <guid>https://forem.com/jjn1056/pagiserver-performance-and-hardening-l8d</guid>
      <description>&lt;p&gt;Some raw info on how PAGI::Server, the reference implementation of the PAGI spec (&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi&lt;/a&gt;) is coming along performance wise.   Here's some high concurrency testing on a basic 'Hello world' app: &lt;a href="https://github.com/jjn1056/pagi/blob/main/examples/01-hello-http/app.pl" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi/blob/main/examples/01-hello-http/app.pl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This testing is running off my MacBook Pro, Intel era (2.4 GHz 8-Core Intel Core i9) which is not particularly known for being a great server.   Running as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; LIBEV_FLAGS=8 ./bin/pagi-server --workers 16 --quiet --no-access-log --loop EV  ./examples/01-hello-http/app.pl 

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

&lt;/div&gt;



&lt;p&gt;Results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% hey -z 30s -c 500 http://localhost:5000/

Summary:
  Total:    30.0217 secs
  Slowest:  0.1110 secs
  Fastest:  0.0097 secs
  Average:  0.0312 secs
  Requests/sec: 16010.2544


Response time histogram:
  0.010 [1] |
  0.020 [376]   |
  0.030 [222649]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.040 [226482]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.050 [26058] |■■■■■
  0.060 [4279]  |■
  0.070 [492]   |
  0.081 [123]   |
  0.091 [173]   |
  0.101 [20]    |
  0.111 [2] |


Latency distribution:
  10% in 0.0249 secs
  25% in 0.0273 secs
  50% in 0.0304 secs
  75% in 0.0340 secs
  90% in 0.0381 secs
  95% in 0.0414 secs
  99% in 0.0505 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0000 secs, 0.0097 secs, 0.1110 secs
  DNS-lookup:   0.0000 secs, 0.0000 secs, 0.0193 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0091 secs
  resp wait:    0.0311 secs, 0.0096 secs, 0.1110 secs
  resp read:    0.0000 secs, 0.0000 secs, 0.0035 secs

Status code distribution:
  [200] 480655 responses

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

&lt;/div&gt;



&lt;p&gt;This beats a similar PSGI hello world running under Starman by 30%, but the important bit to note is that PAGI::Server successfully responded to all requests, whereas Starman fell over by 80% at this load on my machine.   That's why you need to run Starman behind an edge server like Nginx; it just can't take the high concurrency.&lt;/p&gt;

&lt;p&gt;I've also been doing http/websockets compliance and security testing on PAGI::Server, working draft is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jjn1056/pagi/blob/main/lib/PAGI/Server/Compliance.pod" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi/blob/main/lib/PAGI/Server/Compliance.pod&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;volunteers who know a lot about flogging servers very welcomed to help me test this.   In production you are still likely to run behind a proxy or edge server like Ngnix but the more robust it is stand alone the better.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>performance</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>In Defense of Rudolph (and Against State-Sanctioned Reindeer Bullying)</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 14 Dec 2025 13:28:34 +0000</pubDate>
      <link>https://forem.com/jjn1056/in-defense-of-rudolph-and-against-state-sanctioned-reindeer-bullying-32gp</link>
      <guid>https://forem.com/jjn1056/in-defense-of-rudolph-and-against-state-sanctioned-reindeer-bullying-32gp</guid>
      <description>&lt;p&gt;Every year around Christmas, we are expected—&lt;em&gt;required&lt;/em&gt;, really—to cheerfully sing along to &lt;em&gt;Rudolph the Red-Nosed Reindeer&lt;/em&gt;, a song that is widely regarded as wholesome, heartwarming, and suitable for children. It’s so culturally entrenched that it didn’t just become a hit song—it spawned a beloved stop-motion movie that gets replayed annually as a kind of unquestioned holiday ritual.&lt;/p&gt;

&lt;p&gt;This is a lie.&lt;/p&gt;

&lt;p&gt;If you actually listen to the lyrics, or worse, watch the movie, &lt;em&gt;Rudolph&lt;/em&gt; is not a story about inclusion or kindness. It is a story about &lt;strong&gt;state-sanctioned bullying&lt;/strong&gt;, conditional acceptance, and the moral philosophy that &lt;em&gt;you are only worthy of dignity if you are useful to power&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let’s review the facts.&lt;/p&gt;

&lt;p&gt;Rudolph is born with a mutation. Not a choice. Not a moral failing. A physical difference he did nothing to earn. His nose glows. That’s it. That’s the crime.&lt;/p&gt;

&lt;p&gt;The response?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“All of the other reindeer&lt;br&gt;&lt;br&gt;
Used to laugh and call him names&lt;br&gt;&lt;br&gt;
They never let poor Rudolph&lt;br&gt;&lt;br&gt;
Join in any reindeer games”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not subtle. This is open, communal bullying. Not just teasing—&lt;strong&gt;systemic exclusion&lt;/strong&gt;. There is no adult intervention. No North Pole HR department. No reindeer anti-harassment training. Everyone sees this happening and shrugs.&lt;/p&gt;

&lt;p&gt;Including Santa.&lt;/p&gt;

&lt;p&gt;And this matters, because Santa is not a kindly grandpa in this universe. He is a &lt;strong&gt;dictator-for-life&lt;/strong&gt;, controlling labor, housing, and social standing at the North Pole. He runs a command economy powered by elf labor and reindeer logistics. He absolutely has the authority to shut this down.&lt;/p&gt;

&lt;p&gt;He does not.&lt;/p&gt;

&lt;p&gt;Instead, the message is clear: &lt;em&gt;conform or suffer&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Rudolph is ostracized, isolated, and taught that his difference is shameful. The song doesn’t even pretend otherwise. It treats this as normal. Character-building, even.&lt;/p&gt;

&lt;p&gt;But then—&lt;em&gt;miraculously&lt;/em&gt;—a crisis occurs.&lt;/p&gt;

&lt;p&gt;Fog.&lt;/p&gt;

&lt;p&gt;The supply chain is threatened.&lt;/p&gt;

&lt;p&gt;Christmas itself (read: the regime’s legitimacy) is at risk.&lt;/p&gt;

&lt;p&gt;And suddenly—&lt;em&gt;suddenly&lt;/em&gt;—Rudolph’s mutation is no longer disgusting. It’s &lt;strong&gt;strategic&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Then one foggy Christmas Eve&lt;br&gt;&lt;br&gt;
Santa came to say…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note the phrasing. Santa &lt;em&gt;came to say&lt;/em&gt;. Not to apologize. Not to reflect. Not to acknowledge years of cruelty. He simply rebrands Rudolph’s condition as an asset &lt;em&gt;now that it is useful&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Rudolph is allowed to participate—not because he deserves dignity as a being, but because his abnormality can be leveraged to achieve &lt;strong&gt;state objectives&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not redemption. This is &lt;strong&gt;instrumentalization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And yes, Rudolph gets applause at the end. The same reindeer who mocked him now “love him.” But only after he proves his value.&lt;/p&gt;

&lt;p&gt;It teaches children that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bullying is fine until the bullied person turns out to be valuable.&lt;/li&gt;
&lt;li&gt;Authority figures don’t owe you protection—only results.&lt;/li&gt;
&lt;li&gt;Difference is tolerated &lt;em&gt;only&lt;/em&gt; when it serves power.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rudolph didn’t change. The world didn’t become kinder. The regime just found a way to exploit what it once despised.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don’t get acceptance for being different.&lt;br&gt;&lt;br&gt;
You get acceptance for being &lt;em&gt;useful&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’re a programmer, this story might feel… familiar.&lt;/p&gt;

&lt;p&gt;A lot of us grew up as the weird kids. Too intense. Too quiet. Too literal. Too obsessed with systems, patterns, or ideas no one else cared about. We missed social cues. We asked annoying questions. We didn’t “join in the reindeer games.”&lt;/p&gt;

&lt;p&gt;We were tolerated at best. Mocked at worst.&lt;/p&gt;

&lt;p&gt;And then, one day, the fog rolled in.&lt;/p&gt;

&lt;p&gt;Suddenly the same traits—hyperfocus, pattern recognition, obsession with correctness, discomfort with ambiguity—were no longer liabilities. They were &lt;strong&gt;marketable&lt;/strong&gt;. Billable. Recruitable.&lt;/p&gt;

&lt;p&gt;The message didn’t change. The &lt;em&gt;context&lt;/em&gt; did.&lt;/p&gt;

&lt;p&gt;You weren’t accepted because the world became kinder. You were accepted because your difference could now be leveraged by companies, platforms, and institutions that once would have happily ignored you.&lt;/p&gt;

&lt;p&gt;That’s not inclusion. That’s conditional mercy.&lt;/p&gt;

&lt;p&gt;This isn’t to say success is bad, or that being valued for your skills is wrong. It’s to say that it’s worth noticing how often dignity arrives &lt;em&gt;after&lt;/em&gt; usefulness—and how rarely it arrives on its own.&lt;/p&gt;

&lt;p&gt;So no, I don’t sing along anymore.&lt;/p&gt;

&lt;p&gt;Because if &lt;em&gt;Rudolph the Red-Nosed Reindeer&lt;/em&gt; is a Christmas story, it’s not about goodwill toward all.&lt;/p&gt;

&lt;p&gt;It’s about how acceptance works in practice.&lt;/p&gt;

&lt;p&gt;And a lot of us deserved better—long before the fog rolled in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author’s note:&lt;/strong&gt; This piece was written collaboratively with an AI. I brought the ideas; it helped me shape them — a process I call vibe thinking.&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>mentalhealth</category>
      <category>watercooler</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The Joy of Code in the Age of Vibe Engineering</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sat, 13 Dec 2025 01:44:23 +0000</pubDate>
      <link>https://forem.com/jjn1056/the-joy-of-code-in-the-age-of-vibe-engineering-49j2</link>
      <guid>https://forem.com/jjn1056/the-joy-of-code-in-the-age-of-vibe-engineering-49j2</guid>
      <description>&lt;p&gt;I’ve always liked programming because I could get the machine to bend to my will.&lt;/p&gt;

&lt;p&gt;Programming has a puzzle-like pleasure to it. Breaking a messy problem down into smaller and smaller pieces. Staring at a system long enough that the shape of the bug finally reveals itself. Digging through hundreds or thousands of files and finding the one thing that’s off. When it works, it gives me a real sense of accomplishment — the same kind some people get from crosswords or jigsaw puzzles.&lt;/p&gt;

&lt;p&gt;A lot of that joy also came from &lt;em&gt;how&lt;/em&gt; I learned to program. I’m entirely self-taught; I never took a programming class in college that I didn’t fail. That doesn’t make me better than anyone else, but it did shape how I think. Learning outside a traditional path forced me to read a lot of other people’s code, to reverse-engineer decisions, and to develop a certain stubborn self-reliance. I think that background gave me edges and perspectives I might not otherwise have had.&lt;/p&gt;

&lt;p&gt;Open source software was central to that journey. It’s how I learned, and contributing back has always mattered to me — not as virtue signaling or résumé building, but as reciprocity. I’ve benefited enormously from OSS over the years, and giving something back still matters more to me than credit ever has. Of all the things I’ve done professionally, that’s what I’m most proud of.&lt;/p&gt;

&lt;p&gt;For a long time I half-jokingly called myself a &lt;em&gt;bare-knuckles programmer&lt;/em&gt;. I’d slog through things out of bloody stubbornness. If I wrote it, I had written every line of it. There was no code in my projects that exceeded my own ability, even if some of it took far longer than it should have when I needed to stretch and expand my ability.&lt;/p&gt;

&lt;p&gt;That relationship to code is changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  From vibe coding to vibe engineering
&lt;/h2&gt;

&lt;p&gt;I don’t love the term &lt;em&gt;vibe coding&lt;/em&gt;, because it suggests you just tell an AI what you want and it spits something out. That’s not what my experience has been like at all.&lt;/p&gt;

&lt;p&gt;What I’m doing feels more like &lt;strong&gt;vibe engineering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s real work. It’s planning, constraint-setting, and taste. It’s knowing how to ask the right questions, how to push back, and how to recognize when something smells wrong. A lot of the effort goes into building a plan collaboratively, then interrogating the output:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why did you do it this way? What does this code actually mean? What assumptions are hiding here?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The work hasn’t disappeared — it’s moved up a level.&lt;/p&gt;

&lt;h2&gt;
  
  
  PAGI and the hollow aftertaste
&lt;/h2&gt;

&lt;p&gt;That shift really hit me during my work on PAGI&lt;br&gt;&lt;br&gt;
(&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;PAGI wasn’t something I casually prompted into existence. I spent well over a year thinking about it: personal research, research with AI, reading and re-reading existing codebases, staring at designs, backing out of approaches that didn’t feel right, and slowly converging on something I believed was worth building. By the time I started writing serious code, I’d already invested a lot of myself into the problem.&lt;/p&gt;

&lt;p&gt;When I did build it, I vibe-engineered essentially the entire project, including some genuinely complex areas around async code and web servers — topics I’m still not an expert in.&lt;/p&gt;

&lt;p&gt;On one level, it was intoxicating. I produced something I’d been thinking about for a long time. I worked in a domain I’d historically avoided. The system works. It’s real and I think it's going to be a valuable addition to the Perl OSS ecosystem.&lt;/p&gt;

&lt;p&gt;And yet the sense of achievement felt just a little hollow.&lt;/p&gt;

&lt;p&gt;Not because it felt dishonest or unearned. More because my &lt;em&gt;relationship&lt;/em&gt; to the code was different. Some of the code in PAGI is code I could not have written from scratch at the time I wrote it. I understand it now — deeply, in fact, because part of the vibe-engineering process for me is reviewing everything and asking detailed, sometimes annoying questions — but it still feels different than having typed every line myself.&lt;/p&gt;

&lt;p&gt;In the past, there was never any code in my projects that was something I “couldn’t have done.” With PAGI, that line was crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorship, credit, and open source discomfort
&lt;/h2&gt;

&lt;p&gt;That crossing raises uncomfortable questions, especially in open source.&lt;/p&gt;

&lt;p&gt;Is this really my work? How should I respond to praise for the work? Is AI a tool here, a collaborator, or something closer to ghostwriting?&lt;/p&gt;

&lt;p&gt;What makes this tricky is that I genuinely don’t care much about credit. What I care about is contributing something useful back to a community I’ve benefited from enormously. Still, even if the ethics feel sound, there’s an internal discomfort that’s hard to ignore. Praise for cleverness or design lands oddly when you know how much of the implementation was dialogued into existence rather than hand-forged.&lt;/p&gt;

&lt;p&gt;I don’t have clean answers here, and I’m not sure I want to pretend that I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mastery, necessity, and selective depth
&lt;/h2&gt;

&lt;p&gt;Part of what complicates all this is realizing that I don’t actually &lt;em&gt;need&lt;/em&gt; to master everything anymore.&lt;/p&gt;

&lt;p&gt;I don’t have a professional need to be an expert in building secure, performant web servers. That’s never been a core job requirement, and it’s unlikely to become one. In that sense, this isn’t fundamentally different from how I use SQL while knowing relatively little about database internals.  And a big part of the reason to write PAGI is to hide a lot of the complexities of asynchronous web programming behind a higher level abstraction so that less knowledgable programmers can actually use it without having to spend a lot of time mastering low level asynchronous primitives.&lt;/p&gt;

&lt;p&gt;And yet, there’s still a part of me that wonders whether I would have been happier mastering those topics the old way — line by line, mistake by mistake.&lt;/p&gt;

&lt;p&gt;That tension is real. Some of it is about craft. Some of it is about identity. And some of it is about timing. I’m in the final stage of my professional career; I’ll likely be letting go of this work sometime in the next ten years or less. In counterpoint I haven’t been this excited about programming since the late 90s — and that excitement sits right next to a quiet grief for what’s changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When vibe engineering feels like mercy
&lt;/h2&gt;

&lt;p&gt;What convinced me this isn’t a simple “AI good / AI bad” story was a very different experience.&lt;/p&gt;

&lt;p&gt;At work, I had to translate several thousand lines of dense, tech-debt-laden Perl into our newer Go-based system. It was unpleasant work — the kind of job I like the least. Lots of dead zones, historical baggage, and brittle logic.&lt;/p&gt;

&lt;p&gt;Vibe engineering that task didn’t produce any existential angst at all. It produced relief.&lt;/p&gt;

&lt;p&gt;The cost of getting it wrong was lower. The emotional burden was lighter. I didn’t feel like I’d lost anything meaningful by not hand-crafting every line. That contrast made something clear: the discomfort isn’t about AI — it’s about where I derive meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking out loud without the paralysis
&lt;/h2&gt;

&lt;p&gt;There’s another gain here that I didn’t fully appreciate at first.&lt;/p&gt;

&lt;p&gt;Like a lot of programmers, I can get stuck in decision paralysis, especially around architecture; a mild form of OCD I imagine.  I’ll spin on tradeoffs, worried about choosing the “wrong” direction. In the past, the cost of being wrong felt high — not just technically, but personally, because it meant committing a lot of my own effort.&lt;/p&gt;

&lt;p&gt;Dialoguing with AI has helped break that cycle.&lt;/p&gt;

&lt;p&gt;Talking things out — even with something that isn’t human — helps me externalize the problem. I can explore options, stress-test ideas, and move forward with less anxiety. Because the personal cost of a wrong turn is lower, I don’t freeze as often. I make decisions knowing I can revise them without paying the same psychological toll.&lt;/p&gt;

&lt;p&gt;That’s a different kind of joy — quieter than pride, but real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where joy lives now
&lt;/h2&gt;

&lt;p&gt;I don’t know what a “great programmer” is anymore. I’m still proudest of my open source work. What feels most unquestionably &lt;em&gt;mine&lt;/em&gt; now isn’t every line of code, but the problems I choose, the slant I put on solutions, and the insistence on doing the smallest fix that isn’t dumb and aiming for simple, understandable code and interfaces — something I’ve told more than a few coworkers over the years.&lt;/p&gt;

&lt;p&gt;The joy hasn’t disappeared. But it’s changed shape.&lt;/p&gt;

&lt;p&gt;It’s less solitary. Less about proving I can do everything myself. More about judgment, framing, and knowing when something is wrong even if I didn’t write it first.&lt;/p&gt;

&lt;p&gt;So when I ask whether there’s still joy in coding in the age of vibe engineering, I don’t mean it rhetorically. I mean it honestly.&lt;/p&gt;

&lt;p&gt;I think the joy survives — but it asks us to let go of some old certainties about authorship, mastery, and what it means for the machine to bend to our will.&lt;/p&gt;

&lt;p&gt;And I’m still learning how to live with that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author’s note:&lt;/strong&gt; This piece was written collaboratively with an AI. I did the thinking; it helped me talk it through; what I'm calling 'vibe thinking'.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>motivation</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>PAGI: ASGI For Perl, or the Spiritual Successor to Plack</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Tue, 02 Dec 2025 20:13:34 +0000</pubDate>
      <link>https://forem.com/jjn1056/pagi-asgi-for-perl-or-the-spiritual-successor-to-plack-3kb9</link>
      <guid>https://forem.com/jjn1056/pagi-asgi-for-perl-or-the-spiritual-successor-to-plack-3kb9</guid>
      <description>&lt;h1&gt;
  
  
  Introducing PAGI: Async Web Development for Perl
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; PAGI (Perl Asynchronous Gateway Interface) is a new specification for async Perl web applications, inspired by Python's ASGI. It supports HTTP, WebSockets, and Server-Sent Events natively, and can wrap existing PSGI applications for backward compatibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Modern web applications need more than traditional request-response cycles. Real-time features like live notifications, collaborative editing, and streaming data require persistent connections. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; for bidirectional communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Sent Events&lt;/strong&gt; for efficient server push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming responses&lt;/strong&gt; for large payloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection lifecycle management&lt;/strong&gt; for resource pooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PSGI, Perl's venerable web server interface, assumes a synchronous world. While frameworks like Mojolicious have built async capabilities on top, there's no shared standard that allows different async frameworks and servers to interoperate.&lt;/p&gt;

&lt;p&gt;PAGI aims to fill that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is PAGI?
&lt;/h2&gt;

&lt;p&gt;PAGI defines a standard interface between async-capable Perl web servers and applications. If you're familiar with Python's ecosystem, think of it as Perl's answer to ASGI.&lt;/p&gt;

&lt;p&gt;A PAGI application is an async coderef with three parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;AsyncAwait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;experimental&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signatures&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

&lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt; &lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
    &lt;span class="c1"&gt;# $scope   - connection metadata (type, headers, path, etc.)&lt;/span&gt;
    &lt;span class="c1"&gt;# $receive - async coderef to get events from the client&lt;/span&gt;
    &lt;span class="c1"&gt;# $send    - async coderef to send events to the client&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$scope-&amp;gt;{type}&lt;/code&gt; tells you what kind of connection you're handling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard HTTP request/response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistent WebSocket connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server-Sent Events stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lifespan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Application startup/shutdown lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A Simple HTTP Example
&lt;/h2&gt;

&lt;p&gt;Here's "Hello World" in raw PAGI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;AsyncAwait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt; &lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
    &lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported: &lt;/span&gt;&lt;span class="si"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{type}&lt;/span&gt;&lt;span class="p"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;ne&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

    &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;type&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http.response.start&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;status&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[['&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&lt;/span&gt;&lt;span class="p"&gt;']],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http.response.body&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from PAGI!&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;more&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000
curl http://localhost:5000/
&lt;span class="c"&gt;# =&amp;gt; Hello from PAGI!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is split into &lt;code&gt;http.response.start&lt;/code&gt; (headers) and &lt;code&gt;http.response.body&lt;/code&gt; (content). This separation enables streaming—send multiple body chunks with &lt;code&gt;more =&amp;gt; 1&lt;/code&gt; before the final &lt;code&gt;more =&amp;gt; 0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSocket Support
&lt;/h2&gt;

&lt;p&gt;WebSockets are first-class citizens in PAGI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt; &lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;')&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket.accept&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket.receive&lt;/span&gt;&lt;span class="p"&gt;')&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
                &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="s"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket.send&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
                    &lt;span class="s"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Echo: &lt;/span&gt;&lt;span class="si"&gt;$msg&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket.disconnect&lt;/span&gt;&lt;span class="p"&gt;')&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported: &lt;/span&gt;&lt;span class="si"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{type}&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event loop pattern is consistent across all connection types: await events from &lt;code&gt;$receive&lt;/code&gt;, send responses via &lt;code&gt;$send&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  PSGI Compatibility
&lt;/h2&gt;

&lt;p&gt;One of PAGI's key features is backward compatibility with PSGI. The &lt;code&gt;PAGI::App::WrapPSGI&lt;/code&gt; adapter lets you run existing PSGI applications on a PAGI server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::App::&lt;/span&gt;&lt;span class="nv"&gt;WrapPSGI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Your existing Catalyst/Dancer/Plack app&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$psgi_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;psgi_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::App::&lt;/span&gt;&lt;span class="nv"&gt;WrapPSGI&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;psgi_app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$psgi_app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$wrapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;to_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper handles all the translation: building &lt;code&gt;%env&lt;/code&gt; from PAGI scope, collecting request bodies, and converting responses back to PAGI events.&lt;/p&gt;

&lt;p&gt;This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run legacy applications on a PAGI server&lt;/li&gt;
&lt;li&gt;Add WebSocket endpoints alongside existing routes&lt;/li&gt;
&lt;li&gt;Migrate incrementally from PSGI to PAGI&lt;/li&gt;
&lt;li&gt;Share connection pools between old and new code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PAGI::Simple Micro-Framework
&lt;/h2&gt;

&lt;p&gt;For rapid development, PAGI ships with a micro-framework inspired by Express.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Simple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Simple&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My API&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($c) {&lt;/span&gt;
    &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;Hello, World!&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/users/:id&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($c) {&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;path_params&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;post&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/api/data&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($c) {&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json_body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;received&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;to_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebSockets are equally clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/chat&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($ws) {&lt;/span&gt;
    &lt;span class="nv"&gt;$ws&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($data) {&lt;/span&gt;
        &lt;span class="nv"&gt;$ws&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;("&lt;/span&gt;&lt;span class="s2"&gt;Someone said: &lt;/span&gt;&lt;span class="si"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;");&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PAGI::Simple includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Express-style routing with path parameters&lt;/li&gt;
&lt;li&gt;JSON request/response helpers&lt;/li&gt;
&lt;li&gt;Session management&lt;/li&gt;
&lt;li&gt;Middleware support (CORS, logging, rate limiting, etc.)&lt;/li&gt;
&lt;li&gt;Static file serving&lt;/li&gt;
&lt;li&gt;WebSocket rooms and broadcasting&lt;/li&gt;
&lt;li&gt;SSE channels with pub/sub&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;p&gt;PAGI is currently in &lt;strong&gt;early beta&lt;/strong&gt;. The test suite passes, the examples work, but it hasn't been battle-tested in production.&lt;/p&gt;

&lt;p&gt;What exists today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete PAGI specification&lt;/li&gt;
&lt;li&gt;Reference server implementation (&lt;code&gt;PAGI::Server&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;PAGI::Simple micro-framework&lt;/li&gt;
&lt;li&gt;13 example applications&lt;/li&gt;
&lt;li&gt;PSGI compatibility layer&lt;/li&gt;
&lt;li&gt;483 passing tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers willing to experiment and provide feedback&lt;/li&gt;
&lt;li&gt;Real-world testing&lt;/li&gt;
&lt;li&gt;Framework authors interested in building on PAGI&lt;/li&gt;
&lt;li&gt;Performance profiling and optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/jjn1056/pagi.git
&lt;span class="nb"&gt;cd &lt;/span&gt;pagi
cpanm &lt;span class="nt"&gt;--installdeps&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
prove &lt;span class="nt"&gt;-l&lt;/span&gt; t/

&lt;span class="c"&gt;# Try the examples&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; examples/01-hello-http/app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000
pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; examples/simple-01-hello/app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Perl has excellent async primitives (&lt;code&gt;IO::Async&lt;/code&gt;, &lt;code&gt;Future::AsyncAwait&lt;/code&gt;), but no shared specification for async web applications. Each framework implements its own approach, which limits interoperability.&lt;/p&gt;

&lt;p&gt;PAGI provides that shared foundation. By standardizing on a common interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Servers can focus on performance and protocol handling&lt;/li&gt;
&lt;li&gt;Frameworks can focus on developer experience&lt;/li&gt;
&lt;li&gt;Middleware becomes portable across implementations&lt;/li&gt;
&lt;li&gt;The ecosystem can grow together rather than in isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in the future of async Perl web development, I'd love your feedback. Check out the repository, try the examples, and let me know what you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;github.com/jjn1056/pagi&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PAGI is not yet on CPAN. It's experimental software—please don't use it in production unless you really know what you're doing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>perl</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Effective MCP Servers: Patterns for AI Collaboration</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 07 Sep 2025 22:09:41 +0000</pubDate>
      <link>https://forem.com/jjn1056/building-effective-mcp-servers-patterns-for-ai-collaboration-55gg</link>
      <guid>https://forem.com/jjn1056/building-effective-mcp-servers-patterns-for-ai-collaboration-55gg</guid>
      <description>&lt;h1&gt;
  
  
  Building Effective MCP Servers: Advanced Patterns for Production-Ready AI Collaboration
&lt;/h1&gt;

&lt;p&gt;After developing several Model Context Protocol (MCP) servers and observing successful implementations in the community, I've discovered that creating truly useful tools goes far beyond just exposing API endpoints. The key insight is that &lt;strong&gt;MCP servers aren't just data pipes—they're AI collaboration partners&lt;/strong&gt;. Here are the essential patterns I've learned for building MCP servers that work seamlessly with LLMs in production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential Pattern #1: Always Include a Help Tool
&lt;/h2&gt;

&lt;p&gt;One of the most valuable additions to any MCP server is a comprehensive help tool. This serves both human users and the LLM itself, acting as a "manual" that the AI can reference to understand capabilities and guide users effectively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleHelp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`# User Management MCP Server

This server provides comprehensive user management capabilities. Here's how to use it effectively:

## Available Tools:
- **find_users**: Search for users by name, email, or other criteria
- **get_user_details**: Get comprehensive information about a specific user
- **update_user**: Modify user properties and settings

## Recommended Workflow:
1. Start with find_users to locate the user you're interested in
2. Use get_user_details to get complete information about a specific user
3. Use update_user to make changes if needed

## Search Best Practices:
- Use partial names or email addresses for broader searches
- Be more specific if you get too many results
- Use email searches for exact matches

When helping users, always guide them through this workflow and explain what each tool does.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Essential Pattern #2: JSON Data + Guidance Pattern
&lt;/h2&gt;

&lt;p&gt;This is the most important pattern: return structured JSON data in the first content block using proper MCP format, followed by separate guidance blocks that tell the LLM how to interpret and present the information. This separation makes responses both machine-readable and human-friendly.&lt;/p&gt;

&lt;p&gt;In this example we return guidance to the client LLM depending on if the call to the search API returned too many, none, or 'just right' number of users, including a clear description of what the response means as well as instructions the LLM should convey to the end users about what to do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;searchUsersInDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No users found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;resultCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: No users were found for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Tell the user and suggest they:
1. Try a broader search term
2. Check for typos in the search
3. Use partial names or email addresses
4. Ask if they'd like to search using different criteria`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many results&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;resultCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="na"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refine_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: Too many results were found (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; users). Tell the user to:
1. Use a more specific search term
2. Include more characters in their search
3. Try searching by full email address instead of partial name
4. Consider what specific user they're looking for and provide more context`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Users found successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;resultCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;
          &lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: Present these &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; users to the human in a clear, readable format. Let them know they can:
1. Get more detailed information about any user by asking for their details using the user ID
2. Update any user's information if needed
3. Search again with different criteria if the user they want isn't listed

Always ask which user they'd like to work with next, or if they need help with anything specific.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Essential Pattern #3: Detailed Schema Descriptions
&lt;/h2&gt;

&lt;p&gt;Your tool descriptions and parameter schemas are critical for LLM understanding. Be verbose and specific—the extra detail pays dividends in correct tool usage.&lt;/p&gt;

&lt;p&gt;Your schemas are basically OpenAPI, so feel free to take advantages of using descriptions and examples to teach the client about the meaning of the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;find_users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    Search for users in the system using various criteria. This tool helps locate users 
    before performing other operations on them.

    Use this tool when:
    - A user asks to find someone by name or email
    - You need to locate a user before getting their details or updating them
    - Someone provides partial information about a user

    Returns structured JSON data with user summaries. For complete user information, 
    use get_user_details with a specific user ID.

    Error handling: Returns guidance for no results or too many results.
    See the help tool for complete workflow guidance.
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Search term to find users. Can be:
        - Full or partial first/last name
        - Full or partial email address  
        - Employee ID or username

        Examples: "john", "smith", "john.doe@company.com", "@company.com"`&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Maximum number of results to return (default: 50, max: 100)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;searchTerm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Essential Pattern #4: Design Tool Chains with Structured Responses
&lt;/h2&gt;

&lt;p&gt;Create tools that naturally flow into each other, with clear guidance on the progression and structured data that can be easily processed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchUserFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_not_found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: The user ID &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; doesn't exist. Tell the user:
1. The user might have been deleted or the ID is incorrect
2. They can search for users again using find_users
3. Ask them to double-check the user ID or provide the user's name/email to search again`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User details retrieved successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;lastLogin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastLogin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;recentActivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recentActivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;
            &lt;span class="p"&gt;})),&lt;/span&gt;
            &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;createdDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;accountType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountType&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: Present this user information clearly to the human in a well-formatted, readable way. Let them know they can:

1. **Update this user**: Ask if they want to modify any information (name, email, department, role, status, permissions)
2. **Search for other users**: If they need to find additional users
3. **Get help**: If they're unsure what they can do next

If they want to make changes, guide them to specify exactly what they want to update.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Pattern #1: Strategic Data Format Choices
&lt;/h2&gt;

&lt;p&gt;Choose your response format based on how the data will be used and consumed, but always lead with structured JSON for data responses:&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Data for Processing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAnalyticsReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeframe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateAnalytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeframe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;timeframe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeframe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;generatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;totalUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;activeUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeUsers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;newRegistrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newRegistrations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;loginFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loginFrequency&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;trends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;userGrowth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userGrowth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;engagementChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engagementChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;topDepartments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topDepartments&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;charts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;dailyActivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dailyActivity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;departmentBreakdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;departmentBreakdown&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: This analytics data covers &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timeframe&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Present the key metrics in a digestible summary format. Highlight:
1. The most significant trends (growth/decline)
2. Notable patterns in user behavior  
3. Any concerning metrics that need attention
4. Actionable insights from the data

Ask if they want to drill down into specific metrics or see data for a different time period.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  HTML for Rich Interaction with JSON Data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getInteractiveDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserDashboardData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;dashboardData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;availableActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update_profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change_password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;view_activity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manage_permissions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;div class="user-dashboard" data-user-id="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;
  &amp;lt;h3&amp;gt;Dashboard for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h3&amp;gt;

  &amp;lt;div class="quick-stats"&amp;gt;
    &amp;lt;div class="stat"&amp;gt;
      &amp;lt;span class="label"&amp;gt;Last Login:&amp;lt;/span&amp;gt;
      &amp;lt;span class="value"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class="stat"&amp;gt;
      &amp;lt;span class="label"&amp;gt;Total Sessions:&amp;lt;/span&amp;gt;
      &amp;lt;span class="value"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalSessions&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;div class="actions"&amp;gt;
    &amp;lt;button onclick="showUserActions('&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')"&amp;gt;Available Actions&amp;lt;/button&amp;gt;
    &amp;lt;button onclick="refreshDashboard('&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')"&amp;gt;Refresh Data&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
  function showUserActions(userId) {
    // Display available actions based on the JSON data above
    const actions = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availableActions&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;
    console.log('Available actions for user:', userId, actions);
  }
&amp;lt;/script&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: This interactive dashboard shows key information for the user. The JSON data above contains all the raw information, while the HTML provides a user-friendly interface. Guide the user on what actions they can take based on the availableActions in the JSON response.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Pattern #2: State Management with Structured Context
&lt;/h2&gt;

&lt;p&gt;For complex workflows, maintain context through structured JSON responses.   In this example we assign an auditId to the start of the  workflow.  This ID would need to be linked to your session management tool associated with the MCP client session id that got created when the client first connected to your MCP server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;beginUserAudit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;departmentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auditId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateAuditId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUsersByDepartment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;departmentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auditState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;auditId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;departmentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initialized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;totalUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;processedUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currentStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initialization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;userIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Store audit state&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storeAuditState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auditId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auditState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Audit initialized successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;auditState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;nextActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;continue_audit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_audit_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancel_audit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: An audit has been started with ID &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;auditId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; users. 
        Tell the user the audit is ready and explain they can:
        1. Continue with the audit process using continue_user_audit
        2. Check audit progress anytime with get_audit_status  
        3. Cancel if needed with cancel_audit

        Ask if they want to proceed with the audit.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Pattern #3: Performance and Pagination with Structured Responses
&lt;/h2&gt;

&lt;p&gt;Handle large datasets gracefully with smart pagination and structured metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;searchLargeDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSearchResultCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No results found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="na"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broaden_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check_spelling&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;try_partial_terms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: No results found for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Suggest the user try broader search terms or check for typos.`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many results&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="na"&gt;recommendation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refine_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add_filters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use_specific_terms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;narrow_scope&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: Search returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; results - too many to process. Ask the user to refine their search with more specific terms or filters.`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;searchWithPagination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search completed successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;showing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;nextActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_next_page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refine_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refine_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_details&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: Show results &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in a clear format.
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Let the user know there are more results available and they can ask for the next page.`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All results shown.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
        Guide them to get more details on specific items or refine their search.`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error Recovery with Structured Responses
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resilientToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UNKNOWN_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;guidance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RATE_LIMITED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;guidance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: The system is experiencing high load. Tell the user to try again in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PERMISSION_DENIED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requiredPermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requiredPermissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;guidance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: The user lacks required permissions: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requiredPermissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;. Guide them to contact their administrator.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;guidance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`**Instructions for LLM**: An unexpected error occurred. Apologize and suggest they try again or contact support with error code: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guidance&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monitoring and Observability
&lt;/h3&gt;

&lt;p&gt;Include telemetry in your tools to understand usage patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;instrumentedToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Log successful operation&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool_call_success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sanitizeParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log error with context&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool_call_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sanitizeParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example Workflow Implementation
&lt;/h2&gt;

&lt;p&gt;Here's how these patterns work together in a real interaction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: "What can this tool do?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM Response&lt;/strong&gt; (after calling help tool):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This user management server can help you find, view, and update user information. Here's how to get started:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search for users&lt;/strong&gt; - I can find users by name, email, or other criteria&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get detailed information&lt;/strong&gt; - Once we find a user, I can show you their complete profile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update user information&lt;/strong&gt; - I can help you modify user details, permissions, and settings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Would you like to search for a specific user to get started?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: "Find users named john"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM&lt;/strong&gt; (calls find_users with "john", receives structured JSON response):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I found 8 users with "john" in their name:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;John Smith&lt;/strong&gt; - &lt;a href="mailto:john.smith@company.com"&gt;john.smith@company.com&lt;/a&gt; (ID: 1234)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Department: Engineering, Role: Senior Developer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;John Doe&lt;/strong&gt; - &lt;a href="mailto:j.doe@company.com"&gt;j.doe@company.com&lt;/a&gt; (ID: 1235)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Department: Marketing, Role: Marketing Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Johnson Miller&lt;/strong&gt; - &lt;a href="mailto:johnson.miller@company.com"&gt;johnson.miller@company.com&lt;/a&gt; (ID: 1236)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Department: Sales, Role: Sales Representative&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Johnny Wilson&lt;/strong&gt; - &lt;a href="mailto:johnny.w@company.com"&gt;johnny.w@company.com&lt;/a&gt; (ID: 1237)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Department: IT, Role: System Administrator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;John Anderson&lt;/strong&gt; - &lt;a href="mailto:john.anderson@company.com"&gt;john.anderson@company.com&lt;/a&gt; (ID: 1238)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Department: HR, Role: HR Specialist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which user would you like to get more details about? I can show you their complete profile, recent activity, and permissions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: "Get details for John Smith"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM&lt;/strong&gt; (calls get_user_details with ID 1234, receives structured JSON):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here are the complete details for &lt;strong&gt;John Smith&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Information:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email: &lt;a href="mailto:john.smith@company.com"&gt;john.smith@company.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Department: Engineering&lt;/li&gt;
&lt;li&gt;Role: Senior Developer
&lt;/li&gt;
&lt;li&gt;Status: Active&lt;/li&gt;
&lt;li&gt;Last Login: 2024-01-15 09:30 AM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Permissions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code Repository Access&lt;/li&gt;
&lt;li&gt;Development Environment&lt;/li&gt;
&lt;li&gt;Project Management Tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recent Activity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2024-01-15: Logged into development environment&lt;/li&gt;
&lt;li&gt;2024-01-14: Updated project documentation&lt;/li&gt;
&lt;li&gt;2024-01-13: Submitted code review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is there anything you'd like to update about John's account? I can help you modify his role, department, permissions, or other information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key Takeaways for Production MCP Servers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structured JSON with mimeType&lt;/strong&gt; - Use &lt;code&gt;type: "text"&lt;/code&gt; with &lt;code&gt;mimeType: "application/json"&lt;/code&gt; for all structured data responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate guidance blocks&lt;/strong&gt; - Use additional text content blocks for LLM instructions and context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Help tools are essential&lt;/strong&gt; - Both for users and LLMs to understand capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detailed schemas matter&lt;/strong&gt; - Verbose descriptions help LLMs use tools correctly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design tool chains&lt;/strong&gt; - Make tools flow naturally with structured responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle errors gracefully&lt;/strong&gt; - Return structured error data with guidance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for scale&lt;/strong&gt; - Include pagination metadata in structured responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage state explicitly&lt;/strong&gt; - Use structured context objects for complex workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always include next actions&lt;/strong&gt; - Tell the LLM what the user can do next&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build for resilience&lt;/strong&gt; - Structure error responses consistently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add observability&lt;/strong&gt; - Include logging and monitoring for production use&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is to create responses that are both machine-readable (proper JSON content with mimeType) and human-friendly (through LLM guidance). This dual approach ensures your MCP servers can handle real-world complexity while maintaining excellent user experience.&lt;/p&gt;

&lt;p&gt;Remember: the JSON content tells the LLM &lt;strong&gt;what&lt;/strong&gt; happened, while the guidance blocks tell it &lt;strong&gt;how&lt;/strong&gt; to help the user with that information. Together, they create a powerful AI collaboration experience that feels natural and intuitive to users.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Perl Catalyst: Thoughts on Chained Actions</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Wed, 12 Feb 2025 21:25:50 +0000</pubDate>
      <link>https://forem.com/jjn1056/perl-catalyst-thoughts-on-chained-actions-4ck5</link>
      <guid>https://forem.com/jjn1056/perl-catalyst-thoughts-on-chained-actions-4ck5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Chained Actions in Catalyst give you a lot of power and design flexibility but continue to give newbies to the system a lot of trouble understanding how they work.  In addition when you have a chain of actions that span controllers there is no current method to express or determine that relationship.   In this blog I'll be exploring this issue and propose an idea I'd love to get feedback on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standard Example
&lt;/h2&gt;

&lt;p&gt;Here's a simple example of Catalyst action chaining for an HTTP request of '&lt;a href="https://example.local/tasks/create" rel="noopener noreferrer"&gt;https://example.local/tasks/create&lt;/a&gt;'.  This chain starts in the Root controller, passes into the Tasks controller and finalizes in the Tasks::Task controller.  I will only show the actions most directly relevant to this endpoint.  First the Root controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Example::Controller::Root;

use Moose;
use MooseX::Attributes

extends 'Catalyst::ControllerPerRequest';

## ANY /...
sub root :Chained('/') CaptureArg(0) ($self, $c) { }

  ## ANY /...
  sub private :Chained(root) PathPart('') ($self, $c) {
    return $c-&amp;gt;redirect_to_action('/session/show') &amp;amp;&amp;amp; $c-&amp;gt;detach
      unless $c-&amp;gt;user-&amp;gt;authenticated;
  }

# The order of the Action Roles is important!!
sub end :Action Does('RenderErrors') Does('RenderView') { }

__PACKAGE__-&amp;gt;config(namespace=&amp;gt;'');
__PACKAGE__-&amp;gt;meta-&amp;gt;make_immutable;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the Tasks Controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Example::Controller::Tasks;

use Moose;
use MooseX::Attributes

extends 'Catalyst::ControllerPerRequest';

has 'tasks', (
  is=&amp;gt;'ro',
  lazy=&amp;gt;1,
  default =&amp;gt; sub ($self, $c) {
    return $c-&amp;gt;user-&amp;gt;search_related('tasks')-&amp;gt;with_comments_labels;
  }
);

# ANY /tasks/...
sub root :Chained('../private') PathPart('task') CaptureArgs(0) ($self, $c) { }

  # GET /tasks/list
  sub list :GET Chained('root') PartPart('list') Args(0) QueryModel() ($self, $c, $qm) {
    return $self-&amp;gt;view(tasks =&amp;gt; $self-&amp;gt;tasks-&amp;gt;page($qm-&amp;gt;page));
  }

__PACKAGE__-&amp;gt;meta-&amp;gt;make_immutable;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally the Task::Tasks controller&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Example::Controller::Tasks;

use Moose;
use MooseX::Attributes;
use Types::Standard qw(Int);

extends 'Catalyst::ControllerPerRequest';

has 'tasks' =&amp;gt; (
  is =&amp;gt; 'ro',
  lazy =&amp;gt; 1,
  default =&amp;gt; sub($self, $c) {
    return $c-&amp;gt;controller('Tasks')-&amp;gt;tasks;
  }
);

has 'task' =&amp;gt; (is =&amp;gt; 'rw');

# ANY /tasks/...
sub root :Chained('../root') PathPart('') CaptureArgs ($self, $c) { }

  ### CREATE ACTIONS ###

  # ANY /tasks/...
  sub build :Chained('root') PathPart('') CaptureArgs ($self, $c) {

    $self-&amp;gt;task($self-&amp;gt;tasks-&amp;gt;new_task);
  }

    # ANY /tasks/create/...
    sub setup_create :Chained('build') PathPart('create') CaptureArgs ($self, $c) {
      $self-&amp;gt;view_for('create', task =&amp;gt; $self-&amp;gt;task);
    }

      # GET /tasks/create
      sub show_create :GET Chained('setup_create') PathPart('') Args(0) ($self, $c) { return }

      # POST /tasks/create
      sub create :POST Chained('setup_create') PathPart('') Args(0) BodyModel() QueryModel() ($self, $c, $rm, $q) {
        return $self-&amp;gt;process_request($rm, $q);
      }

__PACKAGE__-&amp;gt;meta-&amp;gt;make_immutable;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example creating a URL that has actions chained across three controllers requires quite a lot of non intuitive boilerplate, mostly attributes on the action methods.  In addition the relationship between all three controllers is implicit but not discoverable. The thing I find newcomers get confused about is how to figure out which actions are part of the chain, as well as how to control which part of the URL any given action is trying to match.&lt;/p&gt;

&lt;p&gt;I've been dog fooding code for my personal projects that looks more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Agendum::Controller::Root;

use CatalystX::Object;
use Agendum::Syntax;

extends 'Agendum::Controller';

## ANY /...
sub root :StartAt('/...') ($self, $c) { }

  ## ANY /...
  sub private :Via('root') At('/...') ($self, $c) {
    return $c-&amp;gt;redirect_to_action('/session/show') &amp;amp;&amp;amp; $c-&amp;gt;detach
      unless $c-&amp;gt;user-&amp;gt;authenticated;
  }

# The order of the Action Roles is important!!
sub end :Action Does('RenderErrors') Does('RenderView') { }

__PACKAGE__-&amp;gt;config(namespace=&amp;gt;'');
__PACKAGE__-&amp;gt;meta-&amp;gt;make_immutable;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Agendum::Controller::Tasks;

use CatalystX::Object;
use Agendum::Syntax;

extends 'Agendum::Controller';

parent 'Root';

has 'tasks', (
  is=&amp;gt;'ro',
  lazy=&amp;gt;1,
  default =&amp;gt; sub ($self, $c) {
    return $c-&amp;gt;user-&amp;gt;search_related('tasks')-&amp;gt;with_comments_labels;
  }
);

# ANY /tasks/...
sub root :ViaParent('private') At('/tasks/...')  ($self, $c) { }

  # GET /tasks/list
  sub list :Via('root') Get('list')  QueryModel() ($self, $c, $qm) {
    return $self-&amp;gt;view(tasks =&amp;gt; $self-&amp;gt;tasks-&amp;gt;page($qm-&amp;gt;page));
  }

__PACKAGE__-&amp;gt;meta-&amp;gt;make_immutable;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Agendum::Controller::Tasks::Task;

use CatalystX::Object;
use Agendum::Syntax;
use Types::Standard qw(Int);

extends 'Agendum::Controller';

parent 'Task';

has 'tasks' =&amp;gt; (
  is =&amp;gt; 'ro',
  lazy =&amp;gt; 1,
  default =&amp;gt; sub($self, $c) {
    return $self-&amp;gt;parent-&amp;gt;tasks;
  }
);

has 'task' =&amp;gt; (is =&amp;gt; 'rw');

# ANY /tasks/...
sub root :ViaParent('root') At('/...') ($self, $c) { }

  # ANY /tasks/...
  sub build :Via('root') At('/...') ($self, $c) {
    $self-&amp;gt;task($self-&amp;gt;tasks-&amp;gt;new_task);
  }

    # ANY /tasks/create/...
    sub setup_create :Via('build') At('create/...')  ($self, $c) {
      $self-&amp;gt;view_for('create', task =&amp;gt; $self-&amp;gt;task);
    }

      # GET /tasks/create
      sub show_create :Via('setup_create') Get('')  ($self, $c) { }

      # POST /tasks/create
      sub create :Via('setup_create') Post('')  BodyModel() QueryModel() ($self, $c, $rm, $q) {
        return $self-&amp;gt;process_request($rm, $q);
      }

#... rest of controller

__PACKAGE__-&amp;gt;meta-&amp;gt;make_immutable;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this proposal the chaining attribute syntax is collapsed into basically just two types of attributes; 'Via' (or ViaParent) to indicate the action we are chaining off, and 'At' (or its HTTP Method  versions, 'Get', 'Post', etc), which expresses the part of the Url we are matching.   Additionally we introduced a global keyword 'parent' which indicates the controller which is the 'parent' to the current. This lets us use the distinctive 'ViaParent' to make it clear that the action we are chaining off lies in the parent controller, not the current one. &lt;/p&gt;

&lt;p&gt;The goal here is to both reduce the legacy syntax as well as make it more clear what the flow of actions are.&lt;/p&gt;

&lt;p&gt;Thoughts?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Catalyst Tricks: Map Request Parameters to a Model</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 22 Dec 2024 17:46:38 +0000</pubDate>
      <link>https://forem.com/jjn1056/catalyst-tricks-map-request-parameters-to-a-model-44fa</link>
      <guid>https://forem.com/jjn1056/catalyst-tricks-map-request-parameters-to-a-model-44fa</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Dealing with incoming request parameters (both query and body parameters) is something nearly all Perl Catalyst applications need to cope with.  Unfortunately Catalyst punts here and doesn't give you a lot of guidance and the built in handling leaves a lot to be desired.   In this blog I will first example how the default handling works, some of the problems with it and how Catalyst developers have tried to improve it over the years (with minor success IMHO; I can say that since half the redos are my fault ;)).&lt;/p&gt;

&lt;h2&gt;
  
  
  How Catalyst Handles Request Bodies and Query Parameters
&lt;/h2&gt;

&lt;p&gt;By default incoming query and body parameters get mapped to the Catalyst Request object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $c-&amp;gt;request-&amp;gt;query_parameters
  $c-&amp;gt;request-&amp;gt;body_parameters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;query_parameters&lt;/code&gt; gives you access to parameters passed in the 'query' section of your request URL.  For example if your URL is &lt;code&gt;https://example.com/page/?aaa=1&amp;amp;bbb=2&lt;/code&gt; then &lt;code&gt;query_parameters&lt;/code&gt; will return the following hashref:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  +{
    aaa =&amp;gt; "1",
    bbb =&amp;gt; "2",
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;body_parameters&lt;/code&gt; method gives you access to classic HTML Form POST bodies.  For example if you have an HTML Form like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form action="/login" method="post"&amp;gt;
    &amp;lt;label for="username"&amp;gt;Username:&amp;lt;/label&amp;gt;
    &amp;lt;input type="text" id="username" name="username"&amp;gt;
    &amp;lt;label for="password"&amp;gt;Password:&amp;lt;/label&amp;gt;
    &amp;lt;input type="password" id="password" name="password"&amp;gt;
    &amp;lt;button type="submit"&amp;gt;Login&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user clicks the &lt;code&gt;submit&lt;/code&gt; button you would expect the following hashref in &lt;code&gt;body_parameters&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  +{
    username =&amp;gt; "$USERNAME",
    password =&amp;gt; "$PASSWORD",
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Substitute $USERNAME and $PASSWORD for whatever the user typed into the form).&lt;/p&gt;

&lt;p&gt;Both method return a hashref of key value pairs where the key in the field or parameter and the value is a scalar or arrayref (depending on if there is one or several values for the given field in the request).&lt;/p&gt;

&lt;p&gt;For basic applications this has worked acceptably but there's a number of issues.  First of all the fact that the key can be either a scalar or arrayref is annoying, requiring you to write tons of defensive code like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my $username = $c-&amp;gt;req-&amp;gt;body_parameters-&amp;gt;{username};
$username = ref $username eq 'ARRAY' ? $username[-1] : ($username);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just ignore the problem and potentially open yourself to security issues. Speaking of security issues I don't know how many times I've seen code like this, passing incoming body parameters straight into a  DBIx::Class object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  my $new_user = $c-&amp;gt;model('Schema::User')-&amp;gt;create($c-&amp;gt;req-&amp;gt;body_parameters);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a world of hurt since you are basically passing whatever the user submitted (or your site hacker is submitting) directly to DBIC &lt;code&gt;create&lt;/code&gt;.  You need to be more choosey about the incoming at the very least:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  my $new_user = $c-&amp;gt;model('Schema::User')-&amp;gt;create(
    username =&amp;gt; ref($c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{username}) eq 'ARRAY' ? $c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{username}[-1] : $c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{username},
    password =&amp;gt; ref($c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{password}) eq 'ARRAY' ? $c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{password}[-1] : $c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{password},
  );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At which point you are starting to have a lot of ugly code and you haven't even started on form validation yet.  And with all this repeated code its easy to have a hard to spot typo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  my $new_user = $c-&amp;gt;model('Schema::User')-&amp;gt;create(
    username =&amp;gt; ref($c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{username}) eq 'ARRAY' ? $c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{usrname}[-1] : $c-&amp;gt;req-&amp;gt;body_params-&amp;gt;{usernme},
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've seen a lot of typo issues in Catalyst applications just like this, and they can lead to hard to spot problems since in Perl having a typo in the hash de-reference will not lead to a hard runtime error generally, you just get 'undef' for a value in an unexpected location.   I've seen this problem in Catalyst code which existed for years in the wild. &lt;/p&gt;

&lt;p&gt;Another thing I've seen a lot of is line after line of parameter processing code stuck into controllers.   As it turns out parameter munging is one of the bigger jobs a programmer in a web application can have, especially as the application gets older and you need to introduce new features without breaking backward compatibility.   This can lead to very long and ugly controllers that make following the flow of logic in your request to response cycle difficult. &lt;/p&gt;

&lt;p&gt;You can solve the 'is it a value or an arrayref?' problem by enabled the &lt;code&gt;use_hash_multivalue_in_request&lt;/code&gt; configuration option.  This gives you a Hash::MultiValue object instead of a hashref of request parameters.  Amongst other things it make it easy to say 'when there's more than one value give me only the last one', which is nearly always the right thing as legitimate uses for this typically revolve around HTML Form tricks where some field types like checkboxes don't make it easy to know when the user is explicitly setting an 'off' state.  See &lt;a href="https://metacpan.org/release/JJNAPIORK/Catalyst-Runtime-5.90132/view/lib/Catalyst.pm#CONFIGURATION" rel="noopener noreferrer"&gt;CONFIGURATION&lt;/a&gt; For more.  This however doesn't really help with the other problems such as easy to make typos or dirtying up your controllers with tons of parameter tweaking code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Incoming Request Parameters to a Model
&lt;/h2&gt;

&lt;p&gt;One trick I've used for years when encountering this issue is to use a Catalyst Model as a container for my request parameters.   This model converts the hashref to an actual object with methods, which means any typos get picked up at runtime fast.  This model is also a great place to stick validations and incoming value filters, as well as a good spot to stick and complex logic involving these parameters.  Let's keep this simple and just see how one might do that for the example login form already described:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Example::Model::Params::Login;

use Moose;
use Valiant::Validations;
use Valiant::Filters;

extends 'Catalyst::Model';
with 'Catalyst::Component::InstancePerContext';

sub build_per_context_instance {
  my ($self, $c) = @_;
  my $body = ref($self)-&amp;gt;new(%{$c-&amp;gt;req-&amp;gt;query_parameters}, ctx=&amp;gt;$c);
  return $body-&amp;gt;validate; # -&amp;gt;validate returns '$self' for chaining
}

has ctx =&amp;gt; (is=&amp;gt;'ro');

has username =&amp;gt; (
  is =&amp;gt; 'ro',
  validates =&amp;gt; [
    presence =&amp;gt; 1,
    length =&amp;gt; {
      maximum =&amp;gt; 64,
      minimum =&amp;gt; 1,
    },
  ],
);

has password =&amp;gt; (
  is =&amp;gt; 'ro',
  validates =&amp;gt; [
    presence =&amp;gt; 1,
    length =&amp;gt; {
      maximum =&amp;gt; 64,
      minimum =&amp;gt; 1,
    },
  ],
);

has user =&amp;gt; (
  is =&amp;gt; 'ro',
  lazy =&amp;gt; 1,
  predicate =&amp;gt; 'has_user',
  default =&amp;gt; \&amp;amp;_find_user,
  validates =&amp;gt; [
    presence =&amp;gt; {message=&amp;gt;'User Not Found with credentials.'},
  ],
)

filters_with 'Truncate', max_length=&amp;gt;100;

sub _find_user {
  my $self = shift;
  my $user = $self-&amp;gt;ctx
    -&amp;gt;model('Schema::User')
    -&amp;gt;find({username=&amp;gt;$self-&amp;gt;username});

  return unless $user &amp;amp;&amp;amp; $user-&amp;gt;password_eq($self-&amp;gt;password);  
  return $user;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cleanly encapsulates the entire job of getting the POSTed parameters, making sure they are valid and that the parameters match a user in the database (and that the given password matches the latest in the DB via the &lt;code&gt;password_eq&lt;/code&gt; method, which is an exercise I leave for you; don't forget to hash your passwords in the DB!).&lt;/p&gt;

&lt;p&gt;You can use it in a controller similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Example::Controller::Session;

use Moose;
use MooseX::Attributes;

extends 'Catalyst::Controller';

sub login :Path Args(0) {
  my ($self, $c) = @_;
  my $params = $c-&amp;gt;model('Params::Login');
  return $c-&amp;gt;login_user($params-&amp;gt;user) if $params-&amp;gt;valid;
  return $c-&amp;gt;stash(params =&amp;gt; $params);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example we map the incoming request body to &lt;code&gt;$params&lt;/code&gt; and if the object is valid we perform the login workflow (via &lt;code&gt;login_user($user)&lt;/code&gt;, a method that again I leave to your imagination but probably involves storing the user id in the session and redirecting to some sort of "You're Logged in" page).  If it not we stick &lt;code&gt;$params&lt;/code&gt; in the stash and let the view inspect it for the errors, displaying such to the user in whatever view system you prefer.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In real life you'd probably use the Authentication plugin for something like this, but the general idea here can map to basically any type of incoming query or request bodies, even those via APIs requests that might be in JSON rather than a form POST.  What you get is a nice, clear separation of concerns that improves code readability and long term maintainability.  I like the idea so much, and started using it so much that I've tried to encapsulate the pattern in &lt;a href="https://metacpan.org/pod/CatalystX::RequestModel" rel="noopener noreferrer"&gt;CatalystX::RequestModel&lt;/a&gt;.  Other approaches on CPAN that can do similar would be &lt;a href="https://metacpan.org/pod/HTML::FormHandler" rel="noopener noreferrer"&gt;HTML::FormHandler&lt;/a&gt;, although that tends to focus more on validation and HTML Form field generation, so might be a bigger hammer than you want.&lt;/p&gt;

&lt;p&gt;Modules mentioned in this blog include &lt;a href="https://metacpan.org/dist/Catalyst-Runtime" rel="noopener noreferrer"&gt;Catalyst&lt;/a&gt; and &lt;a href="https://metacpan.org/pod/Valiant" rel="noopener noreferrer"&gt;Valiant&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Bonus Idea
&lt;/h1&gt;

&lt;p&gt;I often use a similar approach to wrap the Catalyst session (also represented as a hash reference) in a model, to offer a strongly typed interface to the session.   Can you figure out the code for that?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>perl</category>
      <category>catalyst</category>
    </item>
  </channel>
</rss>
