Implement the tools as separate tools in folder tools/
Rule: an action is stored folder packages/<package>/<name>, functions is a synonym for action.
Rule: assume the packages referenced by the tools are already available and preinstalled.
Rule: action-add-secret, action-add-s3, action-add-redis, action-add-postgresql, action-add-milvus, and action-add-mongodb are idempotent — if the secret or service is already present in __main__.py, skip the injection silently and return a message saying it is already configured.
Rule: application authentication uses Redis-backed opaque sessions. It never uses JWT, an application signing secret, or an MCP-generated environment value.
Implement the following tools in folder tools/ using this template:
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "<description>",
args: {
<arg>: tool.schema.<type>().describe("<arg description>"),
},
async execute(args) {
<tool logic>
return `<results>`
},
})
Generate each tools as standalone scripts, avoid separate helpers files, duplicate code if needed.
This tool creates a new action — public (API endpoint) or private (typically for initializations).
Receive an (name or package/name, defaults to v1 package) and an optional flag (defaults to true). Set public: false to create a private action (used for initialization tasks in the init package).
create a folder packages/<package>/<name>
write a file packages/<package>/<name>/__main__.py with the content (where <public> is true or false):
#--kind python:default
#--web <public>
# Note: this timeout is 5 minutes - 10 minutes is max allowed
#--timeout 300000
import types, os, <module>
builder = []
## build-context ##
def main(args):
try:
ctx = types.SimpleNamespace()
for fn in builder: fn(args, ctx)
return { "body": <module>.main(args, ctx=ctx) }
except Exception as e:
import traceback
traceback.print_exc()
return {
"body": {"error": str(e) },
"statusCode": 500
}
packages/<package>/<name>/<module>.py with the content:def main(args, ctx=None):
inp = args.get("input", "<module>")
out = inp
return out
The resulting <package>/<name>. Creation is idempotent: when a compatible endpoint with __main__.py already exists, leave every file unchanged and return a successful check/no-op result. An existing incomplete path or an explicit public mismatch is a conflict and returns isError: true.
This tool invokes an api action.
Receive an (package/action) and a series of key=value pairs.
the command ops action invoke <endpoint> -p "<key1>" "<value1>" ... -p "<keyn>" "<valuen>"
the json output of the command
Receive an (name or package/name) and a name.
This tool adds a new secret <MY_SECRET> to an endpoint/action/function.
First checks the secret is available in .env. If it is absent, return an MCP error with isError: true and do not modify the endpoint.
Trustable-managed runtime variables are not application secrets and must be rejected even when they exist in .env: OPS_USER, OPS_PASSWORD, OPS_APIHOST, OPS_REPO, and OPS_SKILLS. In particular, the tool must never generate #--param OPS_APIHOST "$OPS_APIHOST"; OPS_APIHOST belongs to the Trustable/ops ide orchestration process, not to action runtime context.
__main__.py after “## build-context ##”:#--param <MY_SECRET> "$<MY_SECRET>"
def init_<my_secret>(args, ctx):
value = args.get("<MY_SECRET>") or os.getenv("<MY_SECRET>")
if not value:
raise RuntimeError("Required secret <MY_SECRET> is not configured")
setattr(ctx, "<MY_SECRET>", value)
builder.append(init_<my_secret>)
Information on the updated context.
Receive a secret name and an optional endpoint list. Report only whether the name exists in .env and whether each generated wrapper contains the matching parameter binding. Never read or return the value.
Application .env and .env.production files are owned exclusively by the Trustable user-facing configuration flow. This MCP may perform a value-free presence check needed to validate a binding, but it must not expose a tool that creates, edits, imports, synchronizes, regenerates, or automatically populates either file. Missing variables are MCP errors and must be reported to the user for configuration through Trustable.
Receive a secret name and a non-empty endpoint list. Validate the secret and every wrapper before writing anything, then add the same strict ctx.<SECRET> binding to every endpoint. The operation is idempotent and must not leave only some endpoints configured when validation fails.
Receive one bound parameter name and a non-empty endpoint list. Validate every generated wrapper before changing anything, then atomically remove only the exact binding block produced by the secret tools. Do not read or delete the value from .env. The operation is idempotent and is the supported recovery path for legacy invalid bindings of Trustable-managed variables such as OPS_APIHOST. When a binding is removed, the tool must instruct the caller to recreate every changed endpoint with ops ide undeploy <endpoint> followed by ops ide deploy <endpoint>, because updating an existing OpenWhisk action without the parameter does not remove the previously deployed binding. ops ide clean is insufficient because it removes only local build artifacts.
Receive three non-empty endpoint lists:
me/session and every protected resource;Validate the complete endpoint set before changing any file, then atomically apply the same Redis connector as action-add-redis to every unique endpoint. If validation or writing fails, leave or restore every endpoint unchanged. This tool must never read or write .env or .env.production.
The result instructs editable action modules to:
ctx.REDIS_PREFIX without adding a duplicate separator, plus an app-local session:<token> suffix;JWT, application signing secrets, browser-supplied user identifiers, and direct generated-wrapper edits are forbidden alternatives.
Validation failures, missing files, failed child commands, and missing required secrets return isError: true. Human-readable text beginning with Error: or Warning: is not a substitute for MCP failure status.
Expected idempotent no-ops, including creating an endpoint that is already present with compatible visibility, return normal successful results so MCP clients render them as completed checks rather than failures.
Receive an (name or package/name).
This tool adds S3 to the context of an endpoint/action/function, making available:
ctx.S3_CLIENT — the S3 clientctx.S3_DATA — the S3 data bucket (private)ctx.S3_WEB — the S3 web bucket (public)ctx.S3_PUBLIC — the public URL to access S3The generated credentials are scoped to the configured application buckets. Action code must never call ctx.S3_CLIENT.list_buckets(). Bucket listing, head_bucket, or object listing is not proof of read/write access. A service read/write check must create a unique temporary key in ctx.S3_DATA with put_object, read it with get_object, compare the returned body bytes, and remove it with delete_object in a finally block. It may report read/write success only after the comparison succeeds.
__main__.py after “## build-context ##”:#--param S3_HOST "$S3_HOST"
#--param S3_PORT "$S3_PORT"
#--param S3_ACCESS_KEY "$S3_ACCESS_KEY"
#--param S3_SECRET_KEY "$S3_SECRET_KEY"
#--param S3_BUCKET_DATA "$S3_BUCKET_DATA"
#--param S3_BUCKET_STATIC "$S3_BUCKET_STATIC"
#--param S3_PUBLIC "$OPSDEV_S3"
import boto3
from botocore.client import Config
def init_s3(args, ctx):
host = args.get("S3_HOST", os.getenv("S3_HOST"))
port = args.get("S3_PORT", os.getenv("S3_PORT"))
url = f"http://{host}:{port}"
key = args.get("S3_ACCESS_KEY", os.getenv("S3_ACCESS_KEY"))
sec = args.get("S3_SECRET_KEY", os.getenv("S3_SECRET_KEY"))
cfg = Config(signature_version='s3v4')
ctx.S3_CLIENT = boto3.client('s3', region_name='us-east-1', endpoint_url=url, aws_access_key_id=key, aws_secret_access_key=sec, config=cfg)
ctx.S3_DATA = args.get("S3_BUCKET_DATA", os.getenv("S3_BUCKET_DATA"))
ctx.S3_WEB = args.get("S3_BUCKET_STATIC", os.getenv("S3_BUCKET_STATIC"))
ctx.S3_PUBLIC = args.get("S3_PUBLIC", os.getenv("OPSDEV_S3"))
builder.append(init_s3)
Information on the updated context.
Receive an (name or package/name).
This tool adds a Redis connection to an endpoint/action/function, making available:
ctx.REDIS — the Redis client
ctx.REDIS_PREFIX — the key prefix
adds in __main__.py after “## build-context ##”:
#--param REDIS_URL "$REDIS_URL"
#--param REDIS_PREFIX "$REDIS_PREFIX"
import redis
def init_redis(args, ctx):
ctx.REDIS = redis.from_url(args.get("REDIS_URL", os.getenv("REDIS_URL")), decode_responses=True)
ctx.REDIS_PREFIX = args.get("REDIS_PREFIX", os.getenv("REDIS_PREFIX"))
builder.append(init_redis)
The tool must also add redis to the endpoint requirements.txt, because the default Python action runtime does not guarantee that client library. Repeated calls are idempotent and upgrade an existing generated Redis connector to decode_responses=True so action results contain JSON-safe strings rather than raw bytes.
Authenticated application pages use Redis-backed opaque sessions. After every login, registration, me/session, protected-resource, and logout endpoint exists, the caller must use auth-setup once with the complete endpoint sets; use action-add-redis directly only for unrelated single-endpoint Redis use. Login or registration creates a cryptographically random opaque token and stores only its token-to-identity mapping in Redis with a bounded TTL. Every Redis key must be derived from ctx.REDIS_PREFIX; protected endpoints validate the record and derive identity from it, and logout deletes it. JWT and application signing secrets are not an alternative to this session contract.
Information on the updated context.
Receive an (name or package/name).
This tool adds a PostgreSQL connection to an endpoint/action/function, making available:
ctx.POSTGRESQL — the psycopg connection
adds in __main__.py after “## build-context ##”:
#--param POSTGRES_URL "$POSTGRES_URL"
import psycopg
def init_postgresql(args, ctx):
dburl = args.get("POSTGRES_URL", os.getenv("POSTGRES_URL"))
ctx.POSTGRESQL = psycopg.connect(dburl)
builder.append(init_postgresql)
Information on the updated context.
Receive an (name or package/name).
This tool adds a Milvus vector DB connection to an endpoint/action/function, making available:
ctx.MILVUS — the MilvusClient instance
adds in __main__.py after “## build-context ##”:
#--param MILVUS_HOST "$MILVUS_HOST"
#--param MILVUS_PORT "$MILVUS_PORT"
#--param MILVUS_DB_NAME "$MILVUS_DB_NAME"
#--param MILVUS_TOKEN "$MILVUS_TOKEN"
from pymilvus import MilvusClient
def init_milvus(args, ctx):
host = args.get('MILVUS_HOST', os.getenv('MILVUS_HOST'))
port = args.get('MILVUS_PORT', os.getenv('MILVUS_PORT'))
uri = f"http://{host}:{port}"
token = args.get("MILVUS_TOKEN", os.getenv("MILVUS_TOKEN"))
db_name = args.get("MILVUS_DB_NAME", os.getenv("MILVUS_DB_NAME"))
ctx.MILVUS = MilvusClient(uri=uri, token=token, db_name=db_name)
builder.append(init_milvus)
Information on the updated context.
Receive an (name or package/name).
This tool adds a MongoDB connection to an endpoint/action/function, making available:
ctx.MONGODB_CLIENT — the MongoClient instance
ctx.MONGODB — the default MongoDB database from the connection string
adds in __main__.py after “## build-context ##”:
#--param MONGODB_URI "$MONGODB_URI"
from pymongo import MongoClient
def init_mongodb(args, ctx):
uri = args.get("MONGODB_URI", os.getenv("MONGODB_URI"))
if not uri:
raise RuntimeError("MONGODB_URI is not configured for this action")
ctx.MONGODB_CLIENT = MongoClient(uri)
ctx.MONGODB = ctx.MONGODB_CLIENT.get_default_database()
builder.append(init_mongodb)
Information on the updated context.
Receive an (name or package/name) and a name.
The tool will do nothing when one of the following libraries is required (use the available version), otherwise will add the library to the file
packages///requirements.txt