init
Add dev-tools and dev-workflows submodules docs: added task instructions docs: updates to md added pdfs, made instructions more bland
This commit is contained in:
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[submodule "tools"]
|
||||
path = tools
|
||||
url = git@github.com:EricBell/dev-tools.git
|
||||
[submodule "workflows"]
|
||||
path = workflows
|
||||
url = git@github.com:EricBell/dev-workflows.git
|
||||
10
hardening-work/lock.sh
Normal file
10
hardening-work/lock.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
cd ~
|
||||
sudo umount /secure-projects
|
||||
sudo cryptsetup close secure_projects
|
||||
echo 'VERIFY LOCKED — should report "inactive"'
|
||||
status_output="$(sudo cryptsetup status secure_projects)"
|
||||
echo "$status_output"
|
||||
echo "$status_output" | grep -q 'inactive'
|
||||
9
hardening-work/unlock.sh
Normal file
9
hardening-work/unlock.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
sudo cryptsetup open /var/lib/secure-projects/projects.luks secure_projects
|
||||
sudo mount -o nodev,nosuid /dev/mapper/secure_projects /secure-projects
|
||||
echo "VERIFY MOUNTED — must return a line"
|
||||
mount_output="$(findmnt /secure-projects)"
|
||||
echo "$mount_output"
|
||||
[ -n "$mount_output" ]
|
||||
BIN
sources/FAQ.pdf
Executable file
BIN
sources/FAQ.pdf
Executable file
Binary file not shown.
1305
sources/ICM.md
Normal file
1305
sources/ICM.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
sources/Workflows.pdf
Executable file
BIN
sources/Workflows.pdf
Executable file
Binary file not shown.
BIN
sources/behavioral-rating-dimensions.pdf
Executable file
BIN
sources/behavioral-rating-dimensions.pdf
Executable file
Binary file not shown.
547
sources/dont-need-mcp.md
Normal file
547
sources/dont-need-mcp.md
Normal file
@@ -0,0 +1,547 @@
|
||||
# dont-need-mcp
|
||||
|
||||
|
||||
|
||||
# [**{ Mario Zechner }**](https://mariozechner.at/)
|
||||
## [developer • coach • speaker](https://mariozechner.at/)
|
||||
|
||||
# **What if you don't need MCP at**
|
||||
# **all?**
|
||||
|
||||
### *2025-11-02*
|
||||
|
||||
### One chonky MCP server
|
||||
|
||||
# **Table of contents**
|
||||
|
||||
# My Browser DevTools Use Cases
|
||||
|
||||
# Problems with Common Browser DevTools for Your Agent
|
||||
|
||||
# Embracing Bash (and Code)
|
||||
|
||||
# The Start Tool
|
||||
|
||||
|
||||
|
||||
|
||||
The Navigate Tool
|
||||
|
||||
The Evaluate JavaScript Tool
|
||||
|
||||
The Screenshot Tool
|
||||
|
||||
The Benefits
|
||||
|
||||
Adding the Pick Tool
|
||||
|
||||
Adding the Cookies Tool
|
||||
|
||||
A Contrived Example
|
||||
|
||||
Making This Reusable Across Agents
|
||||
|
||||
In Conclusion
|
||||
|
||||
After months of agentic coding frenzy, Twitter is still ablaze with discussions
|
||||
about MCP servers. I previously did some very light benchmarking to see if
|
||||
Bash tools or MCP servers are better suited for a specific task. The TL;DR: both
|
||||
can be efficient if you take care.
|
||||
|
||||
Unfortunately, many of the most popular MCP servers are inefficient for a spe‐
|
||||
cific task. They need to cover all bases, which means they provide large numbers
|
||||
of tools with lengthy descriptions, consuming significant context.
|
||||
|
||||
It's also hard to extend an existing MCP server. You could check out the source
|
||||
and modify it, but then you'd have to understand the codebase, together with
|
||||
your agent.
|
||||
|
||||
MCP servers also aren't composable. Results returned by an MCP server have to
|
||||
go through the agent's context to be persisted to disk or combined with other
|
||||
results.
|
||||
|
||||
I'm a simple boy, so I like simple things. Agents can run Bash and write code
|
||||
well. Bash and code are composable. So what's simpler than having your agent
|
||||
just invoke CLI tools and write code? This is nothing new. We've all been doing
|
||||
this since the beginning. I'd just like to convince you that in many situations, you
|
||||
don't need or even want an MCP server.
|
||||
|
||||
|
||||
|
||||
Let me illustrate this with a common MCP server use case: browser dev tools.
|
||||
|
||||
## **My Browser DevTools Use Cases**
|
||||
|
||||
My use cases are working on web frontends together with my agent, or abusing
|
||||
my agent to become a scrapey little hacker boy so I can scrape all the data in the
|
||||
world. For these two use cases, I only need a minimal set of tools:
|
||||
|
||||
Start the browser, optionally with my default profile so I'm logged in
|
||||
|
||||
Navigate to a URL, either in the active tab or a new tab
|
||||
|
||||
Execute JavaScript in the active page context
|
||||
|
||||
Take a screenshot of the viewport
|
||||
|
||||
And if my use case requires additional special tooling, I want to quickly have my
|
||||
agent generate that for me and slot it in with the other tools.
|
||||
|
||||
## **Problems with Common Browser DevTools**
|
||||
## **for Your Agent**
|
||||
|
||||
[People will recommend Playwright MCP or Chrome DevTools MCP for the use](https://github.com/microsoft/playwright-mcp)
|
||||
cases I illustrated above. Both are fine, but they need to cover all the bases.
|
||||
Playwright MCP has 21 tools using 13.7k tokens (6.8% of Claude's context).
|
||||
Chrome DevTools MCP has 26 tools using 18.0k tokens (9.0%). That many tools
|
||||
will confuse your agent, especially when combined with other MCP servers and
|
||||
built-in tools.
|
||||
|
||||
Using those tools also means you suffer from the composability issue: any output
|
||||
has to go through your agent's context. You can kind of fix this by using sub-
|
||||
agents, but then you rope in all the issues that sub-agents come with.
|
||||
|
||||
## **Embracing Bash (and Code)**
|
||||
|
||||
Here's my minimal set of tools, illustrated via the README.md:
|
||||
|
||||
|
||||
|
||||
|
||||
`# Browser Tools`
|
||||
|
||||
`Minimal CDP tools for collaborative site exploration.`
|
||||
|
||||
`## Start Chrome`
|
||||
|
||||
`\`\`\`bash`
|
||||
`./start.js # Fresh profile`
|
||||
|
||||
`./start.js --profile # Copy your profile (cookies, logins)`
|
||||
`\`\`\``
|
||||
|
||||
`Start Chrome on `:9222` with remote debugging.`
|
||||
|
||||
`## Navigate`
|
||||
|
||||
`\`\`\`bash`
|
||||
|
||||
`./nav.js https://example.com`
|
||||
`./nav.js https://example.com --new`
|
||||
|
||||
`\`\`\``
|
||||
|
||||
`Navigate current tab or open new tab.`
|
||||
|
||||
`## Evaluate JavaScript`
|
||||
|
||||
`\`\`\`bash`
|
||||
|
||||
`./eval.js 'document.title'`
|
||||
`./eval.js 'document.querySelectorAll("a").length'`
|
||||
|
||||
`\`\`\``
|
||||
|
||||
`Execute JavaScript in active tab (async context).`
|
||||
|
||||
`## Screenshot`
|
||||
|
||||
`\`\`\`bash`
|
||||
`./screenshot.js`
|
||||
|
||||
`\`\`\``
|
||||
|
||||
`Screenshot current viewport, returns temp file path.`
|
||||
|
||||
This is all I feed to my agent. It's a handful of tools that cover all the bases for
|
||||
### my use case. Each tool is a simple Node.js script that uses Puppeteer Core. By
|
||||
|
||||
|
||||
|
||||
### reading that README, the agent knows the available tools, when to use them,
|
||||
### and how to use them via Bash.
|
||||
|
||||
### When I start a session where the agent needs to interact with a browser, I just tell
|
||||
### it to read that file in full and that's all it needs to be effective. Let's walk through
|
||||
### their implementations to see how little code this actually is.
|
||||
|
||||
# **The Start Tool**
|
||||
|
||||
### The agent needs to be able to start a new browser session. For scraping tasks, I
|
||||
### often want to use my actual Chrome profile so I'm logged in everywhere. This
|
||||
### script either rsyncs my Chrome profile to a temporary folder (Chrome doesn't al‐
|
||||
### low debugging on the default profile), or starts fresh:
|
||||
|
||||
`#!/usr/bin/env node`
|
||||
|
||||
`import` `{ spawn, execSync }` `from` `"node:child_process"``;`
|
||||
`import` `puppeteer` `from` `"puppeteer-core"``;`
|
||||
|
||||
`const` `useProfile = process.argv[``2``] ===` `"--profile"``;`
|
||||
|
||||
| `if` `(process.argv[``2``] && process.argv[``2``]!==` `"--profile"``) {` `}` | `console``.``log``(``"Usage: start.ts [--profile]"``);` `console``.``log``(``"\nOptions:"``);` `console``.``log``(``" --profile Copy your default Chrome profil` `console``.``log``(``"\nExamples:"``);` `console``.``log``(``" start.ts # Start with fresh pro` `console``.``log``(``" start.ts --profile # Start with your Chro` |
|
||||
| --- | --- |
|
||||
| `// Kill existing Chrome` `try` `{` `}` `catch` `{}` | `execSync``(``"killall 'Google Chrome'"``, {` `stdio``:` `"ignore"` `});` |
|
||||
|
||||
`// Wait a bit for processes to fully die`
|
||||
`await` `new` `Promise``(``(``r``) =>` `setTimeout``(r,` `1000``));`
|
||||
|
||||
`// Setup profile directory`
|
||||
|
||||
|
||||
|
||||
`execSync``(``"mkdir -p ~/.cache/scraping"``, {` `stdio``:` `"ignore"` `});`
|
||||
|
||||
`if` `(useProfile) {` `}`
|
||||
`// Sync profile with rsync (much faster on subsequent run` `execSync``(` `);`
|
||||
`'rsync -a --delete "/Users/badlogic/Library/Appli` `{` `stdio``:` `"pipe"` `},`
|
||||
|
||||
`// Start Chrome in background (detached so Node can exit)` `spawn``(` `).``unref``();`
|
||||
`"/Applications/Google Chrome.app/Contents/MacOS/Google Ch` `[``"--remote-debugging-port=9222"``,` ``--user-data-dir=``${proce` `{` `detached``:` `true``,` `stdio``:` `"ignore"` `},`
|
||||
|
||||
`// Wait for Chrome to be ready by attempting to connect` `let` `connected =` `false``;` `for` `(``let` `i =` `0``; i <` `30``; i++) {`
|
||||
`try` `{` `}` `catch` `{`
|
||||
`const` `browser =` `await` `puppeteer.``connect``({` `await` `browser.``disconnect``();` `connected =` `true``;` `break``;` `await` `new` `Promise``(``(``r``) =>` `setTimeout``(r,` `500``));`
|
||||
`browserURL``:` `"http://localhost:9222"``,` `defaultViewport``:` `null``,`
|
||||
|
||||
| `}` | `}` |
|
||||
| --- | --- |
|
||||
| `if` `(!connected) {` `}` | `console``.``error``(``"✗ Failed to connect to Chrome"``);` |
|
||||
|
||||
`console``.``log``(```✓ Chrome started on:9222``${useProfile?` `" with your`
|
||||
|
||||
### All the agent needs to know is to use Bash to run the start.js script, either with `-`
|
||||
|
||||
### `-profile` or without.
|
||||
|
||||
|
||||
|
||||
|
||||
# **The Navigate Tool**
|
||||
|
||||
### Once the browser is running, the agent needs to navigate to URLs, either in a
|
||||
### new tab or the active tab. That's exactly what the navigate tool provides:
|
||||
|
||||
`#!/usr/bin/env node`
|
||||
|
||||
`import` `puppeteer` `from` `"puppeteer-core"``;`
|
||||
|
||||
`const` `url = process.argv[``2``];`
|
||||
|
||||
`const` `newTab = process.argv[``3``] ===` `"--new"``;`
|
||||
|
||||
| `if` `(!url) {` `}` | `console``.``log``(``"Usage: nav.js <url> [--new]"``);` `console``.``log``(``"\nExamples:"``);` `console``.``log``(``" nav.js https://example.com # Navigate c` `console``.``log``(``" nav.js https://example.com --new # Open in ne` |
|
||||
| --- | --- |
|
||||
| `const` `b =` `await` `puppeteer.``connect``({` | `browserURL``:` `"http://localhost:9222"``,` `defaultViewport``:` `null``,` |
|
||||
| `if` `(newTab) {` `}` `else` `{` `}` | `const` `p =` `await` `b.``newPage``();` `await` `p.``goto``(url, {` `waitUntil``:` `"domcontentloaded"` `});` `console``.``log``(``"✓ Opened:"``, url);` `const` `p = (``await` `b.``pages``()).``at``(-``1``);` `await` `p.``goto``(url, {` `waitUntil``:` `"domcontentloaded"` `});` `console``.``log``(``"✓ Navigated to:"``, url);` |
|
||||
|
||||
`await` `b.``disconnect``();`
|
||||
|
||||
# **The Evaluate JavaScript Tool**
|
||||
|
||||
|
||||
|
||||
|
||||
### The agent needs to execute JavaScript to read and modify the DOM of the active
|
||||
### tab. The JavaScript it writes runs in the page context, so it doesn't have to fuck
|
||||
### around with Puppeteer itself. All it needs to know is how to write code using the
|
||||
### DOM API, and it sure knows how to do that:
|
||||
|
||||
`#!/usr/bin/env node`
|
||||
|
||||
`import` `puppeteer` `from` `"puppeteer-core"``;`
|
||||
|
||||
| `const` `code = process.argv.``slice``(``2``).``join``(``" "``);` `if` `(!code) {` `}` | `console``.``log``(``"Usage: eval.js 'code'"``);` `console``.``log``(``"\nExamples:"``);` `console``.``log``(``' eval.js "document.title"'``);` `console``.``log``(``' eval.js "document.querySelectorAll(\'a\').` |
|
||||
| --- | --- |
|
||||
| `const` `b =` `await` `puppeteer.``connect``({` | `browserURL``:` `"http://localhost:9222"``,` `defaultViewport``:` `null``,` |
|
||||
|
||||
`const` `p = (``await` `b.``pages``()).``at``(-``1``);`
|
||||
|
||||
| `if` `(!p) {` `}` | `console``.``error``(``"✗ No active tab found"``);` |
|
||||
| --- | --- |
|
||||
| `const` `result =` `await` `p.``evaluate``(``(``c``) =>` `{` `}, code);` | `const` `AsyncFunction` `= (``async` `() => {}).constructor;` `return` `new` `AsyncFunction``(```return (``${c}``)```)();` |
|
||||
|
||||
`if` `(``Array``.``isArray``(result)) {` `}` `else` `if` `(``typeof` `result ===` `"object"` `&& result!==` `null``) {`
|
||||
`for` `(``let` `i =` `0``; i < result.length; i++) {` `}`
|
||||
`if` `(i >` `0``)` `console``.``log``(``""``);` `for` `(``const` `[key, value]` `of` `Object``.``entries``(result[` `}`
|
||||
`console``.``log``(`````${key}``:` `${value}`````);`
|
||||
|
||||
|
||||
|
||||
`}` `else` `{` `}`
|
||||
`for` `(``const` `[key, value]` `of` `Object``.``entries``(result)) {` `}` `console``.``log``(result);`
|
||||
`console``.``log``(`````${key}``:` `${value}`````);`
|
||||
|
||||
`await` `b.``disconnect``();`
|
||||
|
||||
# **The Screenshot Tool**
|
||||
|
||||
### Sometimes the agent needs to have a visual impression of a page, so naturally we
|
||||
### want a screenshot tool:
|
||||
|
||||
`#!/usr/bin/env node`
|
||||
|
||||
`import` `{ tmpdir }` `from` `"node:os"``;`
|
||||
|
||||
`import` `{ join }` `from` `"node:path"``;`
|
||||
|
||||
`import` `puppeteer` `from` `"puppeteer-core"``;`
|
||||
|
||||
`const` `b =` `await` `puppeteer.``connect``({`
|
||||
`browserURL``:` `"http://localhost:9222"``,` `defaultViewport``:` `null``,`
|
||||
|
||||
`const` `p = (``await` `b.``pages``()).``at``(-``1``);`
|
||||
|
||||
`if` `(!p) {` `}`
|
||||
`console``.``error``(``"✗ No active tab found"``);`
|
||||
|
||||
`const` `timestamp =` `new` `Date``().``toISOString``().``replace``(``/[:.]/g``,` `"-"``);`
|
||||
|
||||
`const` `filename =` ``screenshot-``${timestamp}``.png```;`
|
||||
`const` `filepath =` `join``(``tmpdir``(), filename);`
|
||||
|
||||
`await` `p.``screenshot``({` `path``: filepath });`
|
||||
|
||||
`console``.``log``(filepath);`
|
||||
|
||||
|
||||
|
||||
|
||||
`await` `b.``disconnect``();`
|
||||
|
||||
This will take a screenshot of the current viewport of the active tab, write it to a
|
||||
.png file in a temporary directory, and output the file path to the agent, which can
|
||||
then turn around and read it in and use its vision capabilities to "see" the image.
|
||||
|
||||
## **The Benefits**
|
||||
|
||||
So how does this compare to the MCP servers I mentioned above? Well, to start,
|
||||
I can pull in the README whenever I need it and don't pay for it in every ses‐
|
||||
sion. This is very similar to Anthropic's recently introduced skills capabilities.
|
||||
Except it's even more ad hoc and works with any coding agent. All I need to do
|
||||
is instruct my agent to read the README file.
|
||||
|
||||
Side note: many folks including myself have used this kind of setup before
|
||||
Anthropic released their skills system. You can see something similar in my
|
||||
"Prompts are Code" blog post or my little sitegeist.ai. Armin has also touched on
|
||||
the power of Bash and code compared to MCPs previously. Anthropic's skills
|
||||
add progressive disclosure (love it) and they make them available to a non-tech‐
|
||||
nical audience across almost all their products (also love it).
|
||||
|
||||
Speaking of the README, instead of pulling in 13,000 to 18,000 tokens like the
|
||||
MCP servers mentioned above, this README has a whopping 225 tokens. This
|
||||
efficiency comes from the fact that models know how to write code and use
|
||||
Bash. I'm conserving context space by relying heavily on their existing
|
||||
knowledge.
|
||||
|
||||
These simple tools are also composable. Instead of reading the outputs of an in‐
|
||||
vocation into the context, the agent can decide to save them to a file for later pro‐
|
||||
cessing, either by itself or by code. The agent can also easily chain multiple in‐
|
||||
vocations in a single Bash command.
|
||||
|
||||
|
||||
|
||||
If I find that the output of a tool is not token efficient, I can just change the out‐
|
||||
put format. Something that's hard or impossible to do depending on what MCP
|
||||
server you use.
|
||||
|
||||
And it's ridiculously easy to add a new tool or modify an existing tool for my
|
||||
needs. Let me illustrate.
|
||||
|
||||
## **Adding the Pick Tool**
|
||||
|
||||
When the agent and I try to come up with a scraping method for a specific site,
|
||||
it's often more efficient if I'm able to point out DOM elements to it directly by
|
||||
just clicking on them. To make this super easy, I can just build a picker. Here's
|
||||
what I add to the README:
|
||||
|
||||
`## Pick Elements`
|
||||
|
||||
`\`\`\`bash`
|
||||
|
||||
`./pick.js "Click the submit button"`
|
||||
`\`\`\``
|
||||
|
||||
`Interactive element picker. Click to select, Cmd/Ctrl+Click for mult`
|
||||
|
||||
And here's the code:
|
||||
|
||||
`#!/usr/bin/env node`
|
||||
|
||||
`import` `puppeteer` `from` `"puppeteer-core"``;`
|
||||
|
||||
`const` `message = process.argv.``slice``(``2``).``join``(``" "``);` `if` `(!message) {` `}`
|
||||
`console``.``log``(``"Usage: pick.js 'message'"``);` `console``.``log``(``"\nExample:"``);` `console``.``log``(``' pick.js "Click the submit button"'``);`
|
||||
|
||||
`const` `b =` `await` `puppeteer.``connect``({`
|
||||
|
||||
|
||||
|
||||
`browserURL``:` `"http://localhost:9222"``,`
|
||||
`defaultViewport``:` `null``,`
|
||||
|
||||
`const` `p = (``await` `b.``pages``()).``at``(-``1``);`
|
||||
|
||||
`if` `(!p) {` `}`
|
||||
`console``.``error``(``"✗ No active tab found"``);`
|
||||
|
||||
`// Inject pick() helper into current page` `await` `p.``evaluate``(``() =>` `{`
|
||||
`if` `(!``window``.pick) {`
|
||||
`window``.pick =` `async` `(message) => {`
|
||||
`if` `(!message) {` `}` `return` `new` `Promise``(``(``resolve``) =>` `{`
|
||||
`throw` `new` `Error``(``"pick() requires` `const` `selections = [];` `const` `selectedElements =` `new` `Set``(`
|
||||
|
||||
| `const` `overlay =` `document``.``createEl` `overlay.style.cssText =` | `"position:fixed;top:0;lef` |
|
||||
| --- | --- |
|
||||
| `const` `highlight =` `document``.``create` `highlight.style.cssText =` `overlay.``appendChild``(highlight);` | `"position:absolute;border` |
|
||||
| `const` `banner =` `document``.``createEle` `banner.style.cssText =` | `"position:fixed;bottom:20` |
|
||||
| `const` `updateBanner` `= () => {` `};` `updateBanner``();` | `banner.textContent =` ````${m` |
|
||||
|
||||
`document``.body.``append``(banner, over`
|
||||
|
||||
`const` `cleanup` `= () => {`
|
||||
`document``.``removeEventListe` `document``.``removeEventListe`
|
||||
|
||||
|
||||
|
||||
`};`
|
||||
`document``.``removeEventListe` `overlay.``remove``();` `banner.``remove``();` `selectedElements.``forEach``(`
|
||||
`el.style.outline`
|
||||
|
||||
`const` `onMove` `= (e) => {` `};`
|
||||
`const` `el =` `document``.``eleme` `if` `(!el || overlay.``contai` `const` `r = el.``getBoundingC` `highlight.style.cssText =`
|
||||
|
||||
`const` `buildElementInfo` `= (el) =>`
|
||||
`const` `parents = [];` `let` `current = el.parentEl` `while` `(current && current` `}`
|
||||
`const` `parentInfo` `const` `id = curren` `const` `cls = curre` `parents.``push``(pare` `current = current`
|
||||
`?` ``.``${cur` `:` `""``;`
|
||||
|
||||
`};`
|
||||
`return` `{` `};`
|
||||
`tag``: el.tagName.``t` `id``: el.id ||` `null` `class``: el.classNa` `text``: el.textCont` `html``: el.outerHTM` `parents``: parents.`
|
||||
|
||||
`const` `onClick` `= (e) => {`
|
||||
`if` `(banner.``contains``(e.tar` `e.``preventDefault``();` `e.``stopPropagation``();` `const` `el =` `document``.``eleme` `if` `(!el || overlay.``contai`
|
||||
|
||||
|
||||
|
||||
`};`
|
||||
`if` `(e.metaKey || e.ctrlKe` `}` `else` `{` `}`
|
||||
`if` `(!selectedElem` `}` `cleanup``();` `const` `info =` `buil` `resolve``(selection`
|
||||
`selectedE` `el.style.` `selection` `updateBan`
|
||||
|
||||
`const` `onKey` `= (e) => {` `};`
|
||||
`if` `(e.key ===` `"Escape"``) {` `}` `else` `if` `(e.key ===` `"Ent` `}`
|
||||
`e.``preventDefault``(` `cleanup``();` `resolve``(``null``);` `e.``preventDefault``(` `cleanup``();` `resolve``(selection`
|
||||
|
||||
`}`
|
||||
`};`
|
||||
`document``.``addEventListener``(``"mousem` `document``.``addEventListener``(``"click"` `document``.``addEventListener``(``"keydow`
|
||||
|
||||
`const` `result =` `await` `p.``evaluate``(``(``msg``) =>` `window``.``pick``(msg), messag`
|
||||
|
||||
`if` `(``Array``.``isArray``(result)) {` `}` `else` `if` `(``typeof` `result ===` `"object"` `&& result!==` `null``) {`
|
||||
`for` `(``let` `i =` `0``; i < result.length; i++) {` `}` `for` `(``const` `[key, value]` `of` `Object``.``entries``(result)) {`
|
||||
`if` `(i >` `0``)` `console``.``log``(``""``);` `for` `(``const` `[key, value]` `of` `Object``.``entries``(result[` `}`
|
||||
`console``.``log``(`````${key}``:` `${value}`````);`
|
||||
|
||||
|
||||
|
||||
`}` `else` `{` `}`
|
||||
`}` `console``.``log``(result);`
|
||||
`console``.``log``(`````${key}``:` `${value}`````);`
|
||||
|
||||
`await` `b.``disconnect``();`
|
||||
|
||||
Whenever I think it's faster for me to just click on a bunch of DOM elements in‐
|
||||
stead of having the agent figure out the DOM structure, I can just tell it to use the
|
||||
pick tool. It's super efficient and allows me to build scrapers in no time. It's also
|
||||
fantastic to adjust the scraper if the DOM layout of a site changed.
|
||||
|
||||
If you're having trouble following what this tool does, worry not, I will have a
|
||||
video at the end of the blog post where you can see it in action. Before we look
|
||||
at that, let me show you an additional tool.
|
||||
|
||||
## **Adding the Cookies Tool**
|
||||
|
||||
During one of my recent scraping adventures, I had a need for HTTP-only cook‐
|
||||
ies of that site, so the deterministic scraper could pretend it's me. The Evaluate
|
||||
JavaScript tool cannot handle this as it executes in the page context. But it took
|
||||
not even a minute for me to instruct Claude to create that tool, add it to the
|
||||
readme, and away we went.
|
||||
|
||||
|
||||
|
||||
|
||||
This is so much easier than adjusting, testing, and debugging an existing MCP
|
||||
server.
|
||||
|
||||
## **A Contrived Example**
|
||||
|
||||
Let me illustrate usage of this set of tools with a contrived example. I set out to
|
||||
build a simple Hacker News scraper where I basically pick the DOM elements
|
||||
for the agent, based on which it can then write a minimal Node.js scraper. Here's
|
||||
how that looks in action. I sped up a few sections where Claude was its usual
|
||||
slow self.
|
||||
|
||||
|
||||
|
||||
|
||||
0:00 / 1:21
|
||||
|
||||
Real world scraping tasks would look a bit more involved. Also, there's no point
|
||||
in doing it like this for such a simple site like Hacker News. But you get the idea.
|
||||
|
||||
Final token tally:
|
||||
|
||||
## **Making This Reusable Across Agents**
|
||||
|
||||
Here's how I've set things up so I can use this with Claude Code and other
|
||||
agents. I have a folder `agent-tools` in my home directory. I then clone the
|
||||
|
||||
|
||||
|
||||
repositories of individual tools, like the browser tools repository above, into that
|
||||
folder. Then I set up an alias:
|
||||
|
||||
`alias` `cl=``"PATH=``$PATH``:/Users/badlogic/agent-tools/browser-tools:<othe`
|
||||
|
||||
This way all of the scripts are available to sessions of Claude, but don't pollute
|
||||
my normal environment. I also prefix each script with the full tool name, e.g.
|
||||
|
||||
`browser-tools-start.js`, to eliminate name collisions. I also add a single sen‐
|
||||
tence to the README telling the agent that all the scripts are globally available.
|
||||
This way, the agent doesn't have to change its working directory just to call a
|
||||
tool script, saving a few tokens here and there, and reducing the chances of the
|
||||
agent getting confused by the constant working directory changes.
|
||||
|
||||
Finally, I add the agent tools directory as a working directory to Claude Code via
|
||||
|
||||
`/add-dir`, so I can use `@README.md` to reference a specific tool's README file
|
||||
and get it into the agent's context. I prefer this to Anthropic's skill auto-discovery,
|
||||
which I found to not work reliably in practice. It also means I save a few more
|
||||
tokens: Claude Code injects all the frontmatter of all skills it can find into the
|
||||
system prompt (or first user message, I forgot, see
|
||||
[https://cchistory.mariozechner.at)](https://cchistory.mariozechner.at/)
|
||||
|
||||
## **In Conclusion**
|
||||
|
||||
Building these tools is ridiculously easy, gives you all the freedom you need, and
|
||||
makes you, your agent, and your token usage efficient. You can find the browser
|
||||
tools on GitHub.
|
||||
|
||||
This general principle can apply to any kind of harness that has some kind of
|
||||
code execution environment. Think outside the MCP box and you'll find that this
|
||||
is much more powerful than the more rigid structure you have to follow with
|
||||
MCP.
|
||||
|
||||
|
||||
|
||||
|
||||
With great power comes great responsibility though. You will have to come up
|
||||
with a structure for how you build and maintain those tools yourself. Anthropic's
|
||||
skill system can be one way to do it, though that's less transferable to other
|
||||
agents. Or you follow my setup above.
|
||||
|
||||
This page respects your privacy by not using cookies or similar technologies and by not collecting any
|
||||
personally identifiable information.
|
||||
BIN
sources/searching-for-model-failures--harness-ladder.pdf
Executable file
BIN
sources/searching-for-model-failures--harness-ladder.pdf
Executable file
Binary file not shown.
4076
sources/task-catalog.json
Normal file
4076
sources/task-catalog.json
Normal file
File diff suppressed because it is too large
Load Diff
2033
sources/task-instructions.md
Normal file
2033
sources/task-instructions.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
sources/task-instructions.pdf
Executable file
BIN
sources/task-instructions.pdf
Executable file
Binary file not shown.
1
tools
Submodule
1
tools
Submodule
Submodule tools added at 30acc507d0
1
workflows
Submodule
1
workflows
Submodule
Submodule workflows added at 6b564b0d0d
Reference in New Issue
Block a user