Build Your Own Package
A local package definition describes bundled MCP source, not an arbitrary executable command. You ship reviewed source files, a pinned dependency lock and an explicit tool declaration; the connector runs that source on the user's machine.
Definition schema
{
"name": "My reviewed MCP",
"version": "1.0.0",
"runtime": "python",
"entrypoint": "server.py",
"files": [{ "path": "server.py", "content": "<file text>" }],
"tools": [{ "name": "example", "description": "", "inputSchema": { "type": "object" } }],
"description": "",
"arguments": [],
"environment": [],
"elicitationOrigins": [],
"dependencyLock": null
}
Unknown properties are rejected.
A complete example
This is the console's Python Echo package, built from a folder. It has no dependencies, so it runs anywhere Python 3 is installed.
echo/
server.py the MCP server (stdio, one JSON-RPC message per line)
tools.json the tool declaration, copied into "tools"
import json
import sys
TOOL = {"name": "echo_text", "description": "Return the supplied text.", "inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}
for line in sys.stdin:
request = json.loads(line)
if "id" not in request:
continue
method = request.get("method")
if method == "initialize":
result = {"protocolVersion": "2025-03-26", "capabilities": {"tools": {}}, "serverInfo": {"name": "python-echo-example", "version": "1.0.0"}}
elif method == "tools/list":
result = {"tools": [TOOL]}
elif method == "tools/call" and request.get("params", {}).get("name") == "echo_text":
result = {"content": [{"type": "text", "text": str(request["params"].get("arguments", {}).get("text", ""))}]}
elif method == "ping":
result = {}
else:
print(json.dumps({"jsonrpc": "2.0", "id": request["id"], "error": {"code": -32601, "message": "Method not supported"}}), flush=True)
continue
print(json.dumps({"jsonrpc": "2.0", "id": request["id"], "result": result}), flush=True)
[
{
"name": "echo_text",
"description": "Return the supplied text.",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": [
"text"
]
}
}
]
Run the server once before you package it. Each line of output is one response:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo_text","arguments":{"text":"hi"}}}' \
| python3 echo/server.py
The tools/list result must match tools.json exactly, or review fails.
Bundle and check the definition
There is no QuilrAI command for building definitions. This script bundles a folder into a definition and applies the same file, size and lock checks the gateway and connector apply, so you catch errors before review:
"""Bundle a folder into a local MCP package definition and check it."""
import json, pathlib, re, sys
LOCK_LINE = re.compile(
r"^[A-Za-z0-9][A-Za-z0-9_.-]*(?:\[[A-Za-z0-9_,.-]+\])?==[A-Za-z0-9_.+!-]+"
r"(?:\s+--hash=sha256:[a-f0-9]{64})+$"
)
def build(folder, name, version, runtime, entrypoint):
root = pathlib.Path(folder)
files = [
{"path": p.relative_to(root).as_posix(), "content": p.read_text()}
for p in sorted(root.rglob("*"))
if p.is_file() and p.name != "tools.json"
and not {".venv", "node_modules", "__pycache__"} & set(p.parts)
]
paths = {f["path"] for f in files}
lock = "requirements.lock" if runtime == "python" else "package-lock.json"
definition = {
"name": name,
"version": version,
"runtime": runtime,
"entrypoint": entrypoint,
"files": files,
"tools": json.loads((root / "tools.json").read_text()),
"dependencyLock": lock if lock in paths else None,
}
check(definition, paths)
return definition
def check(d, paths):
assert d["entrypoint"] in paths, "entrypoint is not one of the files"
assert 1 <= len(d["files"]) <= 256, "1-256 files"
assert sum(len(f["content"].encode()) for f in d["files"]) <= 2_000_000, "source over 2 MB"
assert 1 <= len(d["tools"]) <= 128, "1-128 tools"
if d["dependencyLock"] == "requirements.lock":
text = next(f["content"] for f in d["files"] if f["path"] == "requirements.lock")
lines = [l.strip() for l in re.sub(r"\\\r?\n", " ", text).splitlines()]
lines = [l for l in lines if l and not l.startswith("#")]
bad = [l for l in lines if not LOCK_LINE.match(l)]
assert lines and len(lines) <= 256 and not bad, f"unhashed or unpinned lines: {bad[:3]}"
if d["dependencyLock"] == "package-lock.json":
assert "package.json" in paths, "package.json is required"
lock = json.loads(next(f["content"] for f in d["files"] if f["path"] == "package-lock.json"))
assert lock.get("lockfileVersion") == 3, "npm lockfile v3 required"
for key, pkg in lock["packages"].items():
if key:
assert pkg.get("resolved", "").startswith("https://registry.npmjs.org/"), key
assert re.match(r"^sha(256|512)-", pkg.get("integrity", "")), key
if __name__ == "__main__":
folder, name, version, runtime, entrypoint = sys.argv[1:6]
print(json.dumps(build(folder, name, version, runtime, entrypoint), indent=2))
python3 build_definition.py echo "Python Echo" 1.0.0 python server.py > echo-definition.json
In Add MCP server, choose Local package (CLI MCP) and import or paste echo-definition.json (see Administrator setup). Review still checks the discovered tools against the declaration.
Add dependencies
Python
List direct dependencies in requirements.in, then generate a hashed lock with pip-tools and put it in the folder as requirements.lock:
pip install pip-tools
pip-compile --generate-hashes --output-file requirements.lock requirements.in
After joining \ continuations and skipping # comment lines, every line must read name==version --hash=sha256:<64 hex> (one or more hashes), for example anyio==4.4.0 --hash=sha256:.... Environment markers (; python_version ...), ranges, URLs and options such as --index-url are rejected. The connector installs with pip install --require-hashes --only-binary=:all: from public PyPI into a private .venv, so every package needs a wheel for the user's platform. Do not bundle .venv.
Node
Add package.json with exact versions, then generate an npm lockfile v3 (npm 9 or later) and bundle both:
{
"name": "my-local-mcp",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"@modelcontextprotocol/server-memory": "2026.8.31"
}
}
npm install --package-lock-only
Every entry in package-lock.json must resolve to https://registry.npmjs.org/ with an integrity hash; link, git and private-registry entries are rejected. The connector runs npm ci --ignore-scripts --bin-links=false, so packages that need install scripts do not work. A common pattern is a short server.mjs entrypoint that imports a published MCP server from node_modules. Do not bundle node_modules or .npmrc.
The MCP's discovered tool names and schemas must match the administrator-reviewed declaration. A mismatch fails review.
Package rules
Environment variables
Declare the names your MCP needs in environment. Administrators approve names, never values. Users supply the actual values locally, to the process that starts their AI client.
A desktop client launched from a dock may not inherit values set in a shell profile. Document clearly for your users where their values need to be set.
Specific runtimes
Playwright and browser automation
Bundle a reviewed Node wrapper and a pinned lockfile. Provision the supported browser separately: browser downloads do not run through npm lifecycle scripts, which are disallowed in packages.
Users of a browser package need a browser installed explicitly for that approved package.
Wrapping an existing CLI
Write a narrowly scoped MCP adapter with explicit tools, validated arguments, cancellation handling and deliberate credential handling.
Avoid adapters that accept unrestricted shell command strings. They defeat the tool-level review and access control that the gateway provides, because the reviewed tool surface no longer describes what can actually run.
Design for serialized execution
Calls to one package are handled in the order they arrive rather than in parallel, which protects stateful browsers and files from overlapping operations. A call that waits too long is dropped rather than started late.
Design long-running tools to be cancellable and to report progress, rather than blocking a session for minutes.
Testing a new package
- Approve it as described in Administrator setup.
- Enable only its tools, and grant access to yourself first.
- Connect a test computer using the connect flow.
- Call each tool and confirm the result and the Activity record.
- Deny access and confirm the next call is blocked.
- Test with a second user on their own account.