Browse Source

Add playwright skill (#16)

* Add playwright skill

* Refactor Playwright skill for simplicity and consistency
Alvin 6 months ago
parent
commit
ac359cedda

+ 179 - 0
bun.lock

@@ -6,6 +6,7 @@
       "name": "oh-my-opencode-slim",
       "dependencies": {
         "@ast-grep/cli": "^0.40.0",
+        "@modelcontextprotocol/sdk": "^1.25.1",
         "@opencode-ai/plugin": "^1.1.19",
         "@opencode-ai/sdk": "^1.1.19",
         "zod": "^4.1.8",
@@ -36,22 +37,200 @@
 
     "@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/MJ5un7yxlClaaxou9eYl+Kr2xr/yTtYtTq5aLBWjPWA6dmmJ1nAJgx5zKHVuplFXFBrFDQk3paEgAETMTGcrA=="],
 
+    "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
+
+    "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
+
     "@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.21", "", { "dependencies": { "@opencode-ai/sdk": "1.1.21", "zod": "4.1.8" } }, "sha512-oAWVlKG7LACGFYawfdHGMN6e+6lyN6F+zPVncFUB99BrTl/TjELE5gTZwU7MalGpjwfU77yslBOZm4BXVAYGvw=="],
 
     "@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.21", "", {}, "sha512-4M6lBjRPlPz99Rb5rS5ZqKrb0UDDxOT9VTG06JpNxvA7ynTd8C50ckc2NGzWtvjarmxfaAk1VeuBYN/cq2pIKQ=="],
 
     "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
 
+    "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+    "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
+
+    "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+
+    "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
     "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
 
+    "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+    "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+    "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+    "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+    "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+    "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+    "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+    "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
+
+    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+    "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
     "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
 
+    "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+    "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+    "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+    "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+    "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+    "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+    "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+    "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+    "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
+
+    "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+
+    "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+    "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
+
+    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+    "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
+    "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+    "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+    "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+    "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+    "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+    "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+    "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+    "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
+
+    "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+    "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+    "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+    "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+    "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
+
+    "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+    "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
+
+    "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+    "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+    "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+    "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+    "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+    "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+    "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+    "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+    "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+    "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+    "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+    "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+    "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
+
+    "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+    "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
+
+    "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+    "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+    "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+    "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+    "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+    "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+    "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+    "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+    "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+    "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+    "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+    "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+    "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+    "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+    "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
     "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
 
     "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
 
+    "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+    "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+    "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
     "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
 
+    "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
+
     "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
   }
 }

+ 1 - 0
package.json

@@ -42,6 +42,7 @@
   },
   "dependencies": {
     "@ast-grep/cli": "^0.40.0",
+    "@modelcontextprotocol/sdk": "^1.25.1",
     "@opencode-ai/plugin": "^1.1.19",
     "@opencode-ai/sdk": "^1.1.19",
     "zod": "^4.1.8"

+ 3 - 0
src/agents/orchestrator.ts

@@ -53,6 +53,9 @@ background_task(agent="librarian", prompt="How does library X handle Y")
 ## When to Delegate
 - Use the subagent most relevant to the task description.
 - Use background tasks for research or search while you continue working.
+
+## Skills
+- For browser-related tasks (verification, screenshots, scraping, testing), call the "skill" tool with name "playwright" before taking action. Use relative filenames for screenshots (e.g., 'screenshot.png'); they are saved within subdirectories of '/tmp/playwright-mcp-output/'. Use the "skill_mcp" tool to invoke browser actions with camelCase parameters: skillName, mcpName, toolName, and toolArgs.
 </Delegation>
 
 <Workflow>

+ 5 - 0
src/index.ts

@@ -11,6 +11,8 @@ import {
   ast_grep_search,
   ast_grep_replace,
   antigravity_quota,
+  createSkillTools,
+  SkillMcpManager,
 } from "./tools";
 import { loadPluginConfig, type TmuxConfig } from "./config";
 import { createBuiltinMcps } from "./mcp";
@@ -43,6 +45,8 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   const backgroundManager = new BackgroundTaskManager(ctx, tmuxConfig);
   const backgroundTools = createBackgroundTools(ctx, backgroundManager, tmuxConfig);
   const mcps = createBuiltinMcps(config.disabled_mcps);
+  const skillMcpManager = SkillMcpManager.getInstance();
+  const skillTools = createSkillTools(skillMcpManager);
 
   // Initialize TmuxSessionManager to handle OpenCode's built-in Task tool sessions
   const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
@@ -68,6 +72,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ast_grep_search,
       ast_grep_replace,
       antigravity_quota,
+      ...skillTools,
     },
 
     mcp: mcps,

+ 4 - 4
src/mcp/index.ts

@@ -1,12 +1,12 @@
 import { websearch } from "./websearch";
 import { context7 } from "./context7";
 import { grep_app } from "./grep-app";
-import type { RemoteMcpConfig } from "./types";
+import type { McpConfig } from "./types";
 import type { McpName } from "../config";
 
-export type { RemoteMcpConfig } from "./types";
+export type { RemoteMcpConfig, LocalMcpConfig, McpConfig } from "./types";
 
-const allBuiltinMcps: Record<McpName, RemoteMcpConfig> = {
+const allBuiltinMcps: Record<McpName, McpConfig> = {
   websearch,
   context7,
   grep_app,
@@ -17,7 +17,7 @@ const allBuiltinMcps: Record<McpName, RemoteMcpConfig> = {
  */
 export function createBuiltinMcps(
   disabledMcps: readonly string[] = []
-): Record<string, RemoteMcpConfig> {
+): Record<string, McpConfig> {
   return Object.fromEntries(
     Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name))
   );

+ 8 - 0
src/mcp/types.ts

@@ -6,3 +6,11 @@ export type RemoteMcpConfig = {
   headers?: Record<string, string>;
   oauth?: false;
 };
+
+export type LocalMcpConfig = {
+  type: "local";
+  command: string[];
+  environment?: Record<string, string>;
+};
+
+export type McpConfig = RemoteMcpConfig | LocalMcpConfig;

+ 3 - 0
src/tools/index.ts

@@ -15,3 +15,6 @@ export { ast_grep_search, ast_grep_replace } from "./ast-grep";
 
 // Antigravity quota tool
 export { antigravity_quota } from "./quota";
+
+// Skill tools
+export { createSkillTools, SkillMcpManager } from "./skill";

+ 28 - 0
src/tools/skill/builtin.ts

@@ -0,0 +1,28 @@
+import type { SkillDefinition } from "./types";
+
+const playwrightSkill: SkillDefinition = {
+  name: "playwright",
+  description:
+    "MUST USE for any browser-related tasks. Browser automation via Playwright MCP - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.",
+  template: `# Playwright Browser Automation
+
+This skill provides browser automation capabilities via the Playwright MCP server.`,
+  mcpConfig: {
+    playwright: {
+      command: "npx",
+      args: ["@playwright/mcp@latest"],
+    },
+  },
+};
+
+const builtinSkillsMap = new Map<string, SkillDefinition>([
+  [playwrightSkill.name, playwrightSkill],
+]);
+
+export function getBuiltinSkills(): SkillDefinition[] {
+  return Array.from(builtinSkillsMap.values());
+}
+
+export function getSkillByName(name: string): SkillDefinition | undefined {
+  return builtinSkillsMap.get(name);
+}

+ 3 - 0
src/tools/skill/constants.ts

@@ -0,0 +1,3 @@
+export const SKILL_TOOL_DESCRIPTION = `Loads a skill and returns its instructions and available MCP tools. Use this to activate specialized capabilities like Playwright browser automation.`;
+
+export const SKILL_MCP_TOOL_DESCRIPTION = `Invokes a tool exposed by a skill's MCP server. Use after loading a skill to perform actions like browser automation.`;

+ 8 - 0
src/tools/skill/index.ts

@@ -0,0 +1,8 @@
+export { createSkillTools } from "./tools";
+export { SkillMcpManager } from "./mcp-manager";
+export type {
+  SkillDefinition,
+  SkillArgs,
+  SkillMcpArgs,
+  McpServerConfig,
+} from "./types";

+ 294 - 0
src/tools/skill/mcp-manager.ts

@@ -0,0 +1,294 @@
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js";
+import type {
+  ConnectionType,
+  ManagedClient,
+  ManagedHttpClient,
+  ManagedStdioClient,
+  McpServerConfig,
+  SkillMcpClientInfo,
+} from "./types";
+
+function getConnectionType(config: McpServerConfig): ConnectionType {
+  return "url" in config ? "http" : "stdio";
+}
+
+export class SkillMcpManager {
+  private static instance: SkillMcpManager;
+  private clients: Map<string, ManagedClient> = new Map();
+  private pendingConnections: Map<string, Promise<Client>> = new Map();
+  private cleanupInterval: ReturnType<typeof setInterval> | null = null;
+  private readonly IDLE_TIMEOUT = 5 * 60 * 1000;
+
+  private constructor() {
+    this.startCleanupTimer();
+    this.registerProcessCleanup();
+  }
+
+  static getInstance(): SkillMcpManager {
+    if (!SkillMcpManager.instance) {
+      SkillMcpManager.instance = new SkillMcpManager();
+    }
+    return SkillMcpManager.instance;
+  }
+
+  private registerProcessCleanup(): void {
+    const cleanup = () => {
+      for (const [, managed] of this.clients) {
+        try {
+          managed.client.close();
+        } catch {}
+        try {
+          managed.transport.close();
+        } catch {}
+      }
+      this.clients.clear();
+      if (this.cleanupInterval) {
+        clearInterval(this.cleanupInterval);
+        this.cleanupInterval = null;
+      }
+    };
+
+    process.on("exit", cleanup);
+    process.on("SIGINT", () => {
+      cleanup();
+      process.exit(0);
+    });
+    process.on("SIGTERM", () => {
+      cleanup();
+      process.exit(0);
+    });
+  }
+
+  private getClientKey(info: SkillMcpClientInfo): string {
+    return `${info.sessionId}:${info.skillName}:${info.serverName}`;
+  }
+
+  private async createClient(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Client> {
+    const connectionType = getConnectionType(config);
+
+    if (connectionType === "http") {
+      return this.createHttpClient(info, config);
+    }
+
+    return this.createStdioClient(info, config);
+  }
+
+  private async createHttpClient(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Client> {
+    if (!("url" in config)) {
+      throw new Error(
+        `MCP server "${info.serverName}" missing url for HTTP connection.`
+      );
+    }
+
+    const url = new URL(config.url);
+    const requestInit: RequestInit = {};
+    if (config.headers && Object.keys(config.headers).length > 0) {
+      requestInit.headers = config.headers;
+    }
+
+    const transport = new StreamableHTTPClientTransport(url, {
+      requestInit: Object.keys(requestInit).length > 0 ? requestInit : undefined,
+    });
+
+    const client = new Client(
+      { name: `skill-mcp-${info.skillName}-${info.serverName}`, version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    try {
+      await client.connect(transport);
+    } catch (error) {
+      try {
+        await transport.close();
+      } catch {
+        // ignore transport close errors
+      }
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      throw new Error(
+        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`
+      );
+    }
+
+    const managedClient: ManagedHttpClient = {
+      client,
+      transport,
+      skillName: info.skillName,
+      lastUsedAt: Date.now(),
+      connectionType: "http",
+    };
+
+    this.clients.set(this.getClientKey(info), managedClient);
+    this.startCleanupTimer();
+    return client;
+  }
+
+  private async createStdioClient(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Client> {
+    if (!("command" in config)) {
+      throw new Error(
+        `MCP server "${info.serverName}" missing command for stdio connection.`
+      );
+    }
+
+    const transport = new StdioClientTransport({
+      command: config.command,
+      args: config.args || [],
+      env: config.env,
+      stderr: "ignore",
+    });
+
+    const client = new Client(
+      { name: `skill-mcp-${info.skillName}-${info.serverName}`, version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    try {
+      await client.connect(transport);
+    } catch (error) {
+      try {
+        await transport.close();
+      } catch {
+        // ignore transport close errors
+      }
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      throw new Error(
+        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`
+      );
+    }
+
+    const managedClient: ManagedStdioClient = {
+      client,
+      transport,
+      skillName: info.skillName,
+      lastUsedAt: Date.now(),
+      connectionType: "stdio",
+    };
+
+    this.clients.set(this.getClientKey(info), managedClient);
+    this.startCleanupTimer();
+    return client;
+  }
+
+  private async getOrCreateClient(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Client> {
+    const key = this.getClientKey(info);
+    const existing = this.clients.get(key);
+    if (existing) {
+      existing.lastUsedAt = Date.now();
+      return existing.client;
+    }
+
+    const pending = this.pendingConnections.get(key);
+    if (pending) {
+      return pending;
+    }
+
+    const connectionPromise = this.createClient(info, config);
+    this.pendingConnections.set(key, connectionPromise);
+
+    try {
+      return await connectionPromise;
+    } finally {
+      this.pendingConnections.delete(key);
+    }
+  }
+
+  async listTools(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Tool[]> {
+    const client = await this.getOrCreateClient(info, config);
+    const result = await client.listTools();
+    return result.tools;
+  }
+
+  async listResources(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Resource[]> {
+    const client = await this.getOrCreateClient(info, config);
+    const result = await client.listResources();
+    return result.resources;
+  }
+
+  async listPrompts(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig
+  ): Promise<Prompt[]> {
+    const client = await this.getOrCreateClient(info, config);
+    const result = await client.listPrompts();
+    return result.prompts;
+  }
+
+  async callTool(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig,
+    name: string,
+    args: Record<string, unknown>
+  ): Promise<unknown> {
+    const client = await this.getOrCreateClient(info, config);
+    const result = await client.callTool({ name, arguments: args });
+    return result.content;
+  }
+
+  async readResource(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig,
+    uri: string
+  ): Promise<unknown> {
+    const client = await this.getOrCreateClient(info, config);
+    const result = await client.readResource({ uri });
+    return result.contents;
+  }
+
+  async getPrompt(
+    info: SkillMcpClientInfo,
+    config: McpServerConfig,
+    name: string,
+    args: Record<string, string>
+  ): Promise<unknown> {
+    const client = await this.getOrCreateClient(info, config);
+    const result = await client.getPrompt({ name, arguments: args });
+    return result.messages;
+  }
+
+  private startCleanupTimer(): void {
+    if (this.cleanupInterval) return;
+    this.cleanupInterval = setInterval(() => {
+      this.cleanupIdleClients();
+    }, 60_000);
+    this.cleanupInterval.unref();
+  }
+
+  private async cleanupIdleClients(): Promise<void> {
+    const now = Date.now();
+    for (const [key, managed] of this.clients) {
+      if (now - managed.lastUsedAt > this.IDLE_TIMEOUT) {
+        this.clients.delete(key);
+        try {
+          await managed.client.close();
+        } catch {
+          // ignore close errors
+        }
+        try {
+          await managed.transport.close();
+        } catch {
+          // ignore transport close errors
+        }
+      }
+    }
+  }
+}

+ 197 - 0
src/tools/skill/tools.ts

@@ -0,0 +1,197 @@
+import { tool, type ToolDefinition } from "@opencode-ai/plugin";
+import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js";
+import { SKILL_MCP_TOOL_DESCRIPTION, SKILL_TOOL_DESCRIPTION } from "./constants";
+import { getSkillByName, getBuiltinSkills } from "./builtin";
+import type { SkillArgs, SkillMcpArgs, SkillDefinition } from "./types";
+import { SkillMcpManager } from "./mcp-manager";
+
+function formatSkillsXml(skills: SkillDefinition[]): string {
+  if (skills.length === 0) return "";
+
+  const skillsXml = skills
+    .map(skill => {
+      const lines = [
+        "  <skill>",
+        `    <name>${skill.name}</name>`,
+        `    <description>${skill.description}</description>`,
+        "  </skill>",
+      ];
+      return lines.join("\n");
+    })
+    .join("\n");
+
+  return `\n\n<available_skills>\n${skillsXml}\n</available_skills>`;
+}
+
+async function formatMcpCapabilities(
+  skill: SkillDefinition,
+  manager: SkillMcpManager,
+  sessionId: string
+): Promise<string | null> {
+  if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) {
+    return null;
+  }
+
+  const sections: string[] = ["", "## Available MCP Servers", ""];
+
+  for (const [serverName, config] of Object.entries(skill.mcpConfig)) {
+    const info = {
+      serverName,
+      skillName: skill.name,
+      sessionId,
+    };
+
+    sections.push(`### ${serverName}`);
+    sections.push("");
+
+    try {
+      const [tools, resources, prompts] = await Promise.all([
+        manager.listTools(info, config).catch(() => []),
+        manager.listResources(info, config).catch(() => []),
+        manager.listPrompts(info, config).catch(() => []),
+      ]);
+
+      if (tools.length > 0) {
+        sections.push("**Tools:**");
+        sections.push("");
+        for (const t of tools as Tool[]) {
+          sections.push(`#### \`${t.name}\``);
+          if (t.description) {
+            sections.push(t.description);
+          }
+          sections.push("");
+          sections.push("**inputSchema:**");
+          sections.push("```json");
+          sections.push(JSON.stringify(t.inputSchema, null, 2));
+          sections.push("```");
+          sections.push("");
+        }
+      }
+
+      if (resources.length > 0) {
+        sections.push(
+          `**Resources**: ${(resources as Resource[])
+            .map(r => r.uri)
+            .join(", ")}`
+        );
+      }
+
+      if (prompts.length > 0) {
+        sections.push(
+          `**Prompts**: ${(prompts as Prompt[]).map(p => p.name).join(", ")}`
+        );
+      }
+
+      if (tools.length === 0 && resources.length === 0 && prompts.length === 0) {
+        sections.push("*No capabilities discovered*");
+      }
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      sections.push(`*Failed to connect: ${errorMessage.split("\n")[0]}*`);
+    }
+
+    sections.push("");
+    sections.push(
+      `Use \`skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`
+    );
+    sections.push("");
+  }
+
+  return sections.join("\n");
+}
+
+export function createSkillTools(
+  manager: SkillMcpManager
+): { skill: ToolDefinition; skill_mcp: ToolDefinition } {
+  const skills = getBuiltinSkills();
+  const description =
+    SKILL_TOOL_DESCRIPTION + (skills.length > 0 ? formatSkillsXml(skills) : "");
+
+  const skill: ToolDefinition = tool({
+    description,
+    args: {
+      name: tool.schema.string().describe("The skill identifier from available_skills"),
+    },
+    async execute(args: SkillArgs, toolContext) {
+      const sessionId = toolContext?.sessionID
+        ? String(toolContext.sessionID)
+        : "unknown";
+      const skillDefinition = getSkillByName(args.name);
+      if (!skillDefinition) {
+        const available = skills.map(s => s.name).join(", ");
+        throw new Error(
+          `Skill "${args.name}" not found. Available skills: ${available || "none"}`
+        );
+      }
+
+      const output = [
+        `## Skill: ${skillDefinition.name}`,
+        "",
+        skillDefinition.template.trim(),
+      ];
+
+      if (skillDefinition.mcpConfig) {
+        const mcpInfo = await formatMcpCapabilities(
+          skillDefinition,
+          manager,
+          sessionId
+        );
+        if (mcpInfo) {
+          output.push(mcpInfo);
+        }
+      }
+
+      return output.join("\n");
+    },
+  });
+
+  const skill_mcp: ToolDefinition = tool({
+    description: SKILL_MCP_TOOL_DESCRIPTION,
+    args: {
+      skillName: tool.schema.string().describe("Skill name that provides the MCP"),
+      mcpName: tool.schema.string().describe("MCP server name"),
+      toolName: tool.schema.string().describe("Tool name to invoke"),
+      toolArgs: tool.schema.record(tool.schema.string(), tool.schema.any()).optional(),
+    },
+    async execute(args: SkillMcpArgs, toolContext) {
+      const sessionId = toolContext?.sessionID
+        ? String(toolContext.sessionID)
+        : "unknown";
+      const skillDefinition = getSkillByName(args.skillName);
+      if (!skillDefinition) {
+        const available = skills.map(s => s.name).join(", ");
+        throw new Error(
+          `Skill "${args.skillName}" not found. Available skills: ${available || "none"}`
+        );
+      }
+
+      if (!skillDefinition.mcpConfig || !skillDefinition.mcpConfig[args.mcpName]) {
+        throw new Error(
+          `Skill "${args.skillName}" has no MCP named "${args.mcpName}".`
+        );
+      }
+
+      const config = skillDefinition.mcpConfig[args.mcpName];
+      const info = {
+        serverName: args.mcpName,
+        skillName: skillDefinition.name,
+        sessionId,
+      };
+
+      const result = await manager.callTool(
+        info,
+        config,
+        args.toolName,
+        args.toolArgs || {}
+      );
+
+      if (typeof result === "string") {
+        return result;
+      }
+
+      return JSON.stringify(result);
+    },
+  });
+
+  return { skill, skill_mcp };
+}

+ 104 - 0
src/tools/skill/types.ts

@@ -0,0 +1,104 @@
+import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import type { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import type { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+
+/**
+ * Stdio MCP server configuration (local process)
+ */
+export interface StdioMcpServer {
+  type?: "stdio";
+  command: string;
+  args?: string[];
+  env?: Record<string, string>;
+}
+
+/**
+ * HTTP MCP server configuration (remote server)
+ */
+export interface HttpMcpServer {
+  type: "http" | "sse";
+  url: string;
+  headers?: Record<string, string>;
+}
+
+/**
+ * MCP server configuration - either stdio or http
+ */
+export type McpServerConfig = StdioMcpServer | HttpMcpServer;
+
+/**
+ * Skill MCP configuration - map of server names to their configs
+ */
+export type SkillMcpConfig = Record<string, McpServerConfig>;
+
+/**
+ * Skill definition
+ */
+export interface SkillDefinition {
+  name: string;
+  description: string;
+  template: string;
+  mcpConfig?: SkillMcpConfig;
+}
+
+/**
+ * Info for identifying a managed MCP client
+ */
+export interface SkillMcpClientInfo {
+  serverName: string;
+  skillName: string;
+  sessionId: string;
+}
+
+/**
+ * Connection type for managed clients
+ */
+export type ConnectionType = "stdio" | "http";
+
+/**
+ * Base interface for managed MCP clients
+ */
+interface ManagedClientBase {
+  client: Client;
+  skillName: string;
+  lastUsedAt: number;
+  connectionType: ConnectionType;
+}
+
+/**
+ * Managed stdio client
+ */
+export interface ManagedStdioClient extends ManagedClientBase {
+  connectionType: "stdio";
+  transport: StdioClientTransport;
+}
+
+/**
+ * Managed HTTP client
+ */
+export interface ManagedHttpClient extends ManagedClientBase {
+  connectionType: "http";
+  transport: StreamableHTTPClientTransport;
+}
+
+/**
+ * Managed client - either stdio or http
+ */
+export type ManagedClient = ManagedStdioClient | ManagedHttpClient;
+
+/**
+ * Args for the skill tool
+ */
+export interface SkillArgs {
+  name: string;
+}
+
+/**
+ * Args for the skill_mcp tool
+ */
+export interface SkillMcpArgs {
+  skillName: string;
+  mcpName: string;
+  toolName: string;
+  toolArgs?: Record<string, unknown>;
+}