Tinkr

Run PHP inside your own project from any MCP client, locally or over SSH, and get back what every statement returned as structured data, with the queries it ran and a rollback or sandbox guard for production.

Documentation

Tinkr

Tinkr

Run the code. Read the answer.

A desktop runner for Laravel Tinker and plain PHP.
Run a snippet in any project, on this machine or over SSH, and get back the value it returned,
not a wall of text it printed.

Latest release Downloads Platforms MCP server MIT license

Website · Install · Features · MCP server · Development · Contributing

A run's output in Tinkr, and the value it returned

Why Tinkr

php artisan tinker is the fastest way to ask your app a question, and the worst way to read the answer. Tinkr keeps the question and fixes the answer:

  • Values, not text. A collection comes back as a table, a model as a tree, HTML as a rendered frame.
  • See what it cost. Every query the run sent, with its bindings and timing.
  • Touch production without fear. Run a write inside a transaction, look at what changed, then keep it or roll it back.
  • Always fresh. Each run boots a new Tinker session, so the code on disk is the code that runs.

Install

Grab the installer for your platform from the latest release.

PlatformPackage
macOS (Apple silicon).dmg · .zip
WindowsTinkr.Setup.<version>.exe
Linux.AppImage · .deb · .rpm · .tar.gz

You need php on the machine (or server) where your project lives. That's it.

If macOS says Tinkr is damaged and can't be opened, the release you took was built without an Apple signing identity. Move Tinkr.app to /Applications, then drop the flag macOS puts on a download it could not verify:

xattr -dr com.apple.quarantine /Applications/Tinkr.app

Features

Run

ConnectionsLocal projects; Docker, Podman, Sail and Kubernetes containers; any server over SSH, with a key or a password; a container on a server, which is what a Dokploy, Coolify or compose deploy is; and a Laravel Cloud environment. The container and the project inside it are picked from what is running rather than typed. A password is kept in a file only you can read, never shown back, and answered by the app itself. An SSH connection is logged in to once and then reused, so each run pays for the work and not for the handshake, and the classes a snippet names by their short name are read from that server's own project. Tag a connection as production and Tinkr asks before it writes.
Laravel CloudPaste an API token in Settings → Laravel Cloud and Tinkr checks it before keeping it. Then pick an application and one of its environments; the code runs inside that environment through Laravel Cloud's API, with the same readings, queries and dry runs as any other connection. The token needs Applications: View applications and Environments: View environments, View environment instances, Create environment commands, View environment logs; everything else can stay at None. Point the connection at a local checkout of the same app if you want class names completed. Logs and live method completion are not available on Laravel Cloud.
EditorMonaco, with class names and :: method suggestions read straight from your project, local or remote.
Snippet formsTurn a snippet's variables into a form and run it like a tiny admin panel.
Snippets & historySave the code you reuse. Every run is kept across restarts; one click puts it back in the editor.
Tabs, each with its own answerOpen several snippets at once: every tab keeps the result it ran, its printed output and its own run marker, so switching back finds the answer it left rather than the last one anywhere. Each connection keeps its own tabs: move to another project and back, and the documents, the code in them and a run that was going are where you left them — the run is not stopped by the move, and its answer waits for the connection.

What a run does with the code is worth knowing before the first one:

  • A top-level return ends the snippet. Statements after it do not run, and what it returned is the run's value.
  • A snippet runs once. Every statement is evaluated once rather than probed and retried, so a statement that prints, writes or sends does it once.
  • A snippet is parsed before it runs. A syntax error anywhere in it — including inside a function or class declaration — answers with that parse error and executes nothing, which is what php does with a file. A last line without its ; is still accepted.
  • A top-level use statement is an import for the code after it, and an unconditional top-level function declaration is available to the whole snippet: both are compiled before php runs the first line, and the snippet behaves the same way.
  • Values come back as they are: an object stays an object (an empty one is {}, not []), a scalar stays a scalar, and a value JSON cannot carry — a resource, a recursive reference, an unsafe float — is described by a string such as [resource: stream], [recursion] or INF rather than silently becoming null or 0. An object is described after the snippet has ended, so a jsonSerialize that prints prints once and reads its final state, and the same object returned by several statements is serialized once.
  • If the project's command exits while a run is going — tinker crashed, php was killed — the run ends with that exit status straight away instead of waiting out the run timeout.
  • Reconnecting a Laravel Cloud connection ends the wait on this side only; Laravel Cloud keeps running the command it was given.

Read

ReadingsThe returned value as a tree, a table, a graph, or rendered markup, next to the raw output. Copy any table or save it as CSV.
QueriesEvery SQL statement the run sent, with bindings, duration and connection.
Dry runRun inside a transaction you can keep or take back, or a sandboxed one where mail, jobs, HTTP calls, file writes, the cache and the log are recorded instead of happening.
StopA run that is going can be stopped, and the PHP behind it actually stops — inside a container or on a server. Laravel Cloud cannot cancel a command it was given, so stopping a Cloud run ends the wait and says the command keeps running there. A run also gives up on its own after the run timeout you set in Settings, so a snippet that never returns cannot hold the session. While a run is going the editor is read-only, the button counts the wait, and the answer reports how long the whole of it took.
Log viewerYour app's logs (storage/logs or anywhere) in a window of their own. Level, text and time filters search the file, not just loaded pages. Scroll in either direction with your place preserved and only a bounded window held; messages and stack traces stay together across pages. Follow live, including details that arrive after an entry's header.

Log searches scan the selected file or time range on the connection's host. They can take longer than ordinary paging on a large file, but only matching entries are returned. Changing the file or filters supersedes pending reads; reversing direction discards a pending page rather than evicting what you returned to.

Ask

AI completionsComplete snippets with OpenAI, Claude, Ollama, OpenRouter, OpenCode Zen/Go, or your own endpoint.
AI panelAsk about the code in front of you. The last result goes along, @ pulls in project files, and proposed code drops into the editor.
MCP serverLet Claude Code, Cursor, VS Code and friends run code through the same connections you use.
Tinkr in the light paletteThe AI panel proposing a snippet
Dark and light, one palette across the whole appThe AI panel, with your last result as context

MCP server

Tinkr ships a Model Context Protocol server, so the AI tool you already work in can run PHP the way you do: in your project's own Tinker session, locally or over SSH, with the returned value as data.

The easy way: open Settings → MCP. Tinkr lists the clients it knows (Claude Code, Claude Desktop, Cursor, Windsurf/Devin Desktop, VS Code), writes the config for the one you pick, and gives you the JSON for anything else.

By hand:

{
  "mcpServers": {
    "tinkr": {
      "command": "node",
      "args": ["/path/to/tinkr/packages/tinkr-mcp/src/cli.js"]
    }
  }
}

The directory the client starts it in is the project code runs in. No API token, no open window needed.

Tools
ToolWhat it does
tinkr_documentationHow Tinkr works and how to use these tools: the guards, connections, every tool, how to read an answer
tinkr_run_phpRun PHP in a local project or a Laravel Cloud environment: what every statement returned, and what the code printed
tinkr_run_php_over_sshThe same, on a server, through one of Tinkr's SSH connections
tinkr_list_historyThe runs the app kept for a connection: the code, when it ran, how it ended
tinkr_read_runOne kept run in full: what it returned, printed and threw
tinkr_list_logsThe log files of a connection, newest first: name, size, when it was last written
tinkr_tail_logsA connection's logs: the newest window of a file, or the entries that match a level or a term
tinkr_list_connectionsThe connections saved in the app, with the command each session starts with
tinkr_list_snippetsThe snippets saved for a connection
tinkr_save_snippetKeep code as a snippet of a connection
tinkr_delete_snippetDelete a snippet by its id

History is the app's own: the last fifty runs of a connection. A client's own runs are not written to it, because the app owns that file while it runs.

tinkr_run_php and tinkr_run_php_over_ssh take an optional guard:

  • rollback runs the code inside a transaction that is taken back at the end, and reports what that transaction did — the safe way for an agent to look at production without changing it.
  • sandbox is the same transaction with the side-effect drivers swapped for recorders first: mail goes to the array transport, jobs and the bus to their fakes, the HTTP client answers only what it was given, every disk is faked, the cache is an array store and logging records instead of writing. A write that was rolled back cannot still have sent an email. The answer carries what was caught — a count per channel with the first mail and requests named.

A transaction holds the database and nothing else, so sandbox is the guard to name when the code might send, queue, call or write something.

tinkr_documentation is the same for a model: it answers with what the app is, what each guard holds and when to use which, what a connection is and what the production tag changes, every tool, and how to read the envelope. A model that reads it before running anything needs no prior knowledge of Tinkr.

tinkr_tail_logs reads a log without holding it: a window of the newest bytes by default, before/after for the pages beside it, and since/until for a range of moments found in the file itself. A level or a term turns the read into a scan of the whole range, answering with the newest limit matches — so an error far back in a large log is still found. Reading logs is allowed on any connection, production included: it reads a file.

A connection tagged production is refused unless the server was started on that connection (--connection) or with --allow-production. A run there is a write nobody is watching.

Options
OptionWhat it is
--connection <id|name>Run in a connection the app knows, by id or name
--path <directory>The project to run in (default: where the client started it)
--command <command>What the session starts with (default: the connection's own, or php artisan tinker)
--data-dir <directory>Where the app keeps its connections and snippets, if not the default
--timeout <ms>How long a run may take before it is given up (default: 60000)
--allow-productionRun code on connections tagged production. Without it, a production connection is refused unless the server was started on that one connection with --connection

Try it from a terminal to see what a tool answers:

yarn mcp --path /path/to/your/project

When Tinkr has a connection for that directory, the server uses it, so snippets and history are shared with the app. A packaged app carries its own copy of the server outside the asar, where plain node can read it. An AppImage is mounted under a temporary directory that goes away when the app closes, so on that build the settings screen points clients at a copy in the app's own data directory, which survives both a restart and an update.

A run whose command never boots — no artisan in the directory, a platform check that refuses the php — answers as soon as the command exits, with what it printed and the status it left, instead of waiting out the timeout.

Development

git clone https://github.com/nurmohseni/tinkr.git
cd tinkr
yarn
yarn start

yarn start fetches the interface fonts (Switzer and Sentient from Fontshare, not redistributable) and starts the main and renderer processes. Offline, it falls back to system fonts. See CONTRIBUTING.md for the rest.

Tests

yarn test    # unit and component tests, in every package
yarn e2e     # end-to-end: Playwright launches the real app and drives it

The end-to-end suite runs the real main process, preload bridge and renderer, with a real pty behind every run, against a small tinker-compatible project in e2e/fixtures/app. Every test gets a throwaway profile. Container and SSH flows run where Docker or a local sshd is available. e2e/README.md has the coverage and the details; .github/workflows/tests.yml runs both suites on every push and pull request.

Tech stack

A Yarn workspaces monorepo on Electron.

LayerBuilt with
RendererReact, Vite, jotai, Immer, Monaco Editor, xterm.js, wouter, React Hook Form, React Intl
Mainnode-pty, electron-store, ncc
DesignOne system in design.md; tokens in packages/tinkr-renderer/public/tinkr.css. Type: Switzer, Sentient, Commit Mono

Contributing

Bug reports, ideas and pull requests are welcome. Start with CONTRIBUTING.md, or open an issue.

License

MIT