The catalog finds the tool. The hook decides whether it fires.
I called Strands a framework in May. It still is. The reason it belongs in a week about tool middleware is the hook that sits between the model’s intent and the call.
Yesterday’s post was Composio: a hosted catalog, a session scoped to one of your users, seven meta-tools that keep a thousand wrappers out of the prompt. MCP is the connector those wrappers travel over. Strands is the other shape of the same problem. You own the loop. You decorate the functions. You register a callback that can cancel a refund before the SDK ever opens a socket. Nobody is pretending this is npm. It is the in-process gate.
The numbers moved since May, and the repo name moved with them. In May the public tree was strands-agents/sdk-python, version 1.38.0, about 5,800 stars. That URL now 301s to strands-agents/harness-sdk. The monorepo holds the Python SDK, the TypeScript SDK, the docs site, and a bundled MCP server. This morning it sits at 6,990 stars, Apache 2.0, last push a few hours ago. The Python package on PyPI is strands-agents 1.53.0, uploaded August 21. The TypeScript package is @strands-agents/sdk 1.14.0, same day. Cadence since late July has been roughly weekly: 1.50.0 on July 24, 1.51.0 on August 7, 1.52.0 on August 12, 1.53.0 last Friday. That is not a science-fair SDK. It is a product with customers inside Amazon and a public tree that has to keep up.
If a search result still points at github.com/strands-ai/strands-agents-sdk, that repo never existed. You want the monorepo.
I am not going to rerun the May teardown. The model-agnostic claim still holds. I have moved a Strands agent from Bedrock to the Anthropic API without touching the loop or the tool definitions, and that is still the test. OpenTelemetry spans still come out of the box for model calls, tool invocations, and loop iterations. Bedrock is still the default provider. Lambda, Fargate, EKS, and AgentCore still lead the deploy docs. The lock-in is still gravitational, not contractual. You can run this on a laptop with Ollama and never open the AWS console. Most teams that pick it will not.
What changed is where the project spends its engineering. The community tools package, strands-agents-tools 0.8.6, shipped August 7 with a deprecation pass and a warning in the README that the tools are experimental and need an independent security review before production. Sleep, editor, and shell now point at SDK-vended replacements. Batch is gone because concurrent execution is the default. Think, current_time, memory, and retrieve point at native reasoning config, a context injector, or MemoryManager. The maintainers say the repo will eventually be archived. They would rather you use an official vendor MCP server than a second wrapper they have to keep in sync. That is the tell. The interesting surface is no longer a pile of community tools. It is the SDK’s own tool contract, plus MCP as a first-class source, plus the hook that can refuse a call.
A tool in Strands is a typed function. In Python you decorate it. In TypeScript you pass a Zod schema. The model sees a name, a description, and an input schema. Drop a .py file on a path and the Python SDK will load it, which is still the fastest inner loop I have used in this category and still a footgun if the file has hidden state. The model asks for a tool. The executor validates the request, runs the function, and feeds the result back. Failures come back as tool errors, not as exceptions that kill the loop. That part is ordinary. Every serious SDK does a version of it.
The middleware is the event that fires after lookup and before execution.
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
@tool
def issue_refund(order_id: str, amount: float) -> str:
"""Process a customer refund against the billing record."""
return payments.refund(order_id, amount)
def approve_refunds(event: BeforeToolCallEvent):
name = event.tool_use["name"]
if name == "issue_refund":
event.cancel_tool = "Refunds require a human."
agent = Agent(tools=[issue_refund], hooks=[approve_refunds])
agent("Refund order 1842 for $86.40")BeforeToolCallEvent can inspect the call, rewrite it, cancel it, or raise an interrupt and hand control back to a human. BeforeToolsEvent does the same for the whole batch. On resume, completed results are kept so the model is not asked the same question twice. TypeScript names the cancel field event.cancel. Python names it event.cancel_tool. Same gate. The homepage sample uses it to refuse a report that has no citations. I would use it to refuse a write against production unless a ticket id is present in the arguments. That is tool middleware. It is not a catalog. It is policy sitting on the call path you already own.
MCP plugs into the same path. You construct an MCPClient, pass it in the tools list, and the server’s advertised tools become ordinary Strands tools. Stdio for a local process. Streamable HTTP for a hosted endpoint, including Composio’s session.mcp.url from yesterday if you want their catalog without their session object. SSE if that is what the server still speaks. TypeScript can filter by name, regex, or callback, and prefix names when two servers collide. 1.53.0 added client OAuth on streamable HTTP and started surfacing MCP tool annotations on ToolSpec. Python still wants the client used inside a with block. Step outside it and you get MCPClientInitializationError. That is not a footgun you discover in the README. You discover it the first time a request handler returns and the next request tries to reuse the agent.
The split against Composio is clean if you stop asking which product is “the npm of agents.” Composio owns the long tail of SaaS and the OAuth browser dance for a user you have never met. Strands owns the tools that are your product: the refund, the prior-auth packet, the eligibility check, the Lambda that already has an IAM role. You can point a Strands agent at a Composio MCP URL and get both. You should not point it at that URL for the call you will be paged about, because yesterday’s post already said the hosted path skips beforeExecute. Strands’s hook does not skip. If the gate is the point, keep the tool in-process.
The honest limits have not gone away. They have gotten more specific. The loop is still model-driven. When the model picks the wrong tool, you are reading traces, not a graph. LangGraph users who want explicit edges will hate this, and they should pick the other tool. Invocation limits exist now (limit_turns, limit_total_tokens, limit_output_tokens), which is the adult version of the tail-chasing I complained about in May. They are budgets, not a substitute for a state machine. Hot-reload of a tool directory is still a development gift and a production problem. The community package still contains use_aws, a browser, a computer-use tool, and a dynamic mcp_client the README marks with a security warning. An agent that can spawn an MCP server is an agent that can be talked into spawning the wrong one. The protocol does not save you. The allow-list on the client does.
1.53.0 also shipped context-manager offloading, audio content blocks, and agent-as-tool delegation that no longer needs a hand-rolled wrapper. Those are framework features. I am leaving them on the table. They do not change the decision this week. The decision is whether your tool layer is a session in someone else’s catalog or a function in your process with a hook in front of it.
Authentication is still the hole. MCP OAuth in 1.53.0 authenticates the client to the server. It does not rotate the credential the server uses on your behalf, it does not scope that credential to one tenant, and it does not give an examiner a reason code for why the agent was allowed to call issue_refund. Composio generates a browser link a human has to click. Strands will happily use whatever IAM role or environment variable you left in the process. Neither one is agent-scale auth. That is tomorrow.
If you are already on AWS and the agent needs to call internals you write, Strands is the boring correct choice, and boring is the point. If the agent needs Gmail and HubSpot for a user who signed up an hour ago, buy the catalog. If you only need a connector, speak MCP and skip both products. The quiet bet Amazon made is not that the world needed another framework. It is that the call path would become the product, and that the team willing to put a cancel hook on that path would still be standing when the registries finished arguing about who has more wrappers.
The connector is settled. The catalog is a rental. The hook is yours. Tomorrow is the credential the hook still cannot see.
If this was useful, forward it to one engineer who needs less noise in their feed.


