---
title: "Personal MCP OS — 85-tool local execution layer | Harshith Nayaka L"
description: "Local-first MCP execution layer: 85 tools across Windows and Android behind risk-based policy, one-use human approvals and a mandatory audit trail."
canonical: "https://harshith-nayaka-l-portfolio.vercel.app/work/personal-os-mcp"
last-updated: "2026-09-16T02:41:15Z"
author: "Harshith Nayaka L"
content-type: "text/markdown"
html-version: "https://harshith-nayaka-l-portfolio.vercel.app/work/personal-os-mcp"
---
# Personal MCP OS — 85-tool local execution layer

> Gives an MCP client real hands on a machine — filesystem, shell, Git, browser, Android — behind a policy and approval layer that assumes the model will eventually ask for something it shouldn't.

Case study by Harshith Nayaka L, AI Engineer (Full-Stack), Bengaluru, India.
Canonical page: https://harshith-nayaka-l-portfolio.vercel.app/work/personal-os-mcp

- **Role:** Sole architect and engineer
- **Surface:** 85 MCP tools across Windows and Android
- **Transport:** stdio adapter, loopback core, paired devices
- **Status:** Runnable; local-first, no cloud dependency

## The problem

An AI client that can only talk is limited to advice. One that can act is useful — and immediately dangerous, because the thing deciding what to run is a language model and the thing running it is your actual computer, with your files and your logged-in sessions.

The usual answers are both bad. Lock it down to a toy sandbox and it cannot do the work you wanted. Give it unrestricted execution and you have handed a probabilistic system your user account. The interesting engineering is in the middle: full capability, with a boundary that a human controls and can inspect.

## What I built

Personal MCP OS is a local execution layer with three processes. An MCP adapter speaks stdio to the client, one session per connection. A platform-independent core owns the device registry, routing, policy, approvals and audit. Separate agents — a Windows process and a native Kotlin Android companion — do the actual execution and are reached over authenticated transports.

Capability is broad on purpose: 85 tools covering filesystem, shell and PowerShell, Git, a persistent Playwright Chromium, HTTP, clipboard, applications, and on Android the storage-access framework, intents, MediaStore and notification access. The tool reference is generated from the validated capability catalog rather than written by hand, so the documentation cannot drift from what the server actually exposes.

Every capability declares a risk level — READ, WRITE, SENSITIVE or EXECUTE — and every argument shape is a Zod schema with unknown arguments rejected. Policy maps names or risk levels to allow, required or deny, with a capability override beating its risk level, and a missing policy entry requiring approval rather than defaulting open.

## Pipeline

- **Client:** MCP client (Any MCP-compatible client) → stdio adapter (One session per connection)
- **Core:** Validate (Zod schema; unknown args rejected) → Route (Session preference; ambiguity rejected) → Policy (Risk level and per-capability overrides)
- **Approval:** Approval required (Returns a request ID, not a result) → Human approves (Separate admin CLI and credential) → One-use grant (Bound to args, session, device; 5 min)
- **Execute:** Windows agent (Filesystem, shell, Git, Playwright, HTTP) → Android companion (SAF, intents, MediaStore, notifications)
- **Record:** SQLite audit (Mandatory; payload bodies omitted) → Structured result (ok/data or typed error)

## The judgment calls

**The model cannot approve its own request**

When a capability needs approval the call returns a request ID instead of a result. A human inspects the sanitized arguments in a terminal and approves through a separate admin CLI with its own credential — there is no MCP tool that grants approval. The grant is bound to the exact arguments, session, device and capability, expires in five minutes, and is consumed once. Re-running the same call needs a new one.

**Browser interaction is SENSITIVE by default, on principle**

Every click, key press, submit and download-triggering click requires approval, because a generic executor cannot infer what a website action does. There is deliberately no heuristic reading button text to decide a control looks harmless. `browser.evaluate` is implemented and denied by default: arbitrary page JavaScript removes the browser's confidentiality boundary and can reach page credentials.

**The security doc says what the boundary is not**

It states plainly that the program runs with the user's permissions, that approved shell commands, executable launches, Git hooks and page JavaScript can exceed the filesystem roots, and that command deny patterns are not a security boundary. Redaction is described as best effort. An execution layer that allows arbitrary commands cannot promise never to surface an unknown secret, and claiming otherwise would be the actual vulnerability.

**There are no credential-extraction tools, by omission**

Nothing exports cookies, passwords, storage state or auth headers. A persistent browser profile lets the browser use existing sessions without the server ever handing them over. Credentials are encrypted at rest — Windows DPAPI, Android Keystore AES-GCM — and the directories holding them are denied to the filesystem tools. Android notification retrieval returns metadata only, which keeps message bodies and OTPs out of reach.

**Nothing listens beyond loopback**

The core binds to loopback and requires a token on every endpoint except a one-use pairing route gated by a 128-bit, five-minute code. Browser-origin requests are rejected outright. The phone reaches the core through an adb reverse tunnel, so there is no LAN listener and no cloud relay. HTTP primitives resolve, validate and pin the destination address, block private ranges unless explicitly listed, and do not follow redirects.

**Timeout is not rollback**

The docs are explicit that a timed-out command, request or browser action may already have had an effect, and that the caller should check state before retrying a mutation. For a system whose whole job is side effects on a real machine, pretending a deadline undoes work would be the more dangerous simplification.

## What it changed

**What it enables:** An MCP client can work a real machine end to end — read and edit files inside configured roots, run commands, drive Git, operate a persistent browser, and reach across to a paired Android device — without any of it leaving the machine or passing through a hosted service.

**Where the effort actually went:** Not the 85 tools; those are a catalog. The work is the boundary around them: risk classification, policy resolution, one-use approvals bound to exact arguments, canonical path checks for traversal and symlink escape, redaction, and a mandatory audit that records the action while omitting the payload.

**Honest scope:** This is a personal automation tool, not a hostile-code sandbox, and the documentation leads with that. Pairing tokens stay valid until local state is cleared and the daemon restarts — there is no remote revocation in V1. Path checks are not OS-level isolation against a local attacker racing filesystem operations.

## Built with

Model Context Protocol (TypeScript SDK 1.30.0), TypeScript monorepo, Kotlin (Android companion), Playwright Chromium, Zod-validated capability catalog, SQLite audit, Windows DPAPI / Android Keystore

## Links

- [View on GitHub](https://github.com/HarshithNayakaL/personal-os-mcp)
