The curated Packages cover common services, and you can write a Package for your own. For a service that has neither yet, or whose Package lacks a feature you need, there is often an MCP server. Declared in a Blueprint, that server becomes a Package. Submilli reads its tools and their JSON schemas and turns them into a TypeScript library with one typed function per tool. A program imports and calls it like any other Package, under the same rules. The Blueprint says which of its tools a program may call, and the credential stays outside the program.
This guide shows you how to make an MCP server importable as a Package. The examples are Playwright’s server, which needs no account, and Linear’s, which takes an API key or an OAuth login. Substitute your server’s URL and tools.
To follow the Playwright example, start its server first:
npx @playwright/mcp@latest --port 8931 --headless --isolatedDeclare it
Section titled “Declare it”submilli blueprint init browsesubmilli blueprint add-mcp playwright http://localhost:8931/mcp✓ added mcp server 'playwright' (no auth) to blueprint.yaml Gated by `mcp.playwright` (deny by default) — set its action to `allow` (optionally `filter: tool == "..."`) to use it.permissions: main: - capability: mcp.playwright action: denymcp: playwright: url: http://localhost:8931/mcpThe name you give becomes the key in the mcp block, the Package name
@mcp/playwright, and the capability mcp.playwright. The server must
speak MCP over HTTP. If yours speaks it over standard input and output,
put it behind an HTTP endpoint first, as --port does for Playwright’s.
Refer to MCP servers for the block’s
fields.
Allow its tools
Section titled “Allow its tools”The deny rule that add-mcp wrote says nothing default: deny doesn’t,
so drop it first. capability remove takes all the rules for a
capability, so it goes before the grant. One capability covers the
server, and the tool field of a filter picks tools:
submilli blueprint capability remove mcp.playwrightsubmilli blueprint capability add mcp.playwright \ --filter 'tool == "browser_navigate" or tool == "browser_snapshot" or tool == "browser_click"'✓ removed 1 rule(s) for 'mcp.playwright' from caller 'main' in blueprint.yaml✓ added allow mcp.playwright (filter: tool == "browser_navigate" or tool == "browser_snapshot" or tool == "browser_click") to caller 'main' in blueprint.yamlpermissions: main: - capability: mcp.playwright filter: tool == "browser_navigate" or tool == "browser_snapshot" or tool == "browser_click" action: allowThe three tools are allowed and other tools meet the default. To allow
the tools whose names start the same way, use glob, as in
tool glob "list_*". Don’t write the tool into the capability name, as in
mcp.playwright/browser_click, because the Blueprint is refused.
Choose tools by what their arguments can do as well as by their names. A
rule sees the tool’s name and nothing else, and browser_navigate asked
for a javascript: address runs script in the page as browser_evaluate
would. If an allowed tool is that broad, restrict it where the server runs
(Playwright’s takes --allowed-origins), or put a Package in front of it
that checks the arguments and grant the Package instead.
Call it
Section titled “Call it”import playwright from "@mcp/playwright";
function main(): string { playwright.browser_navigate({ url: "https://example.com" }); return playwright.browser_snapshot({}) as string;}submilli run --blueprint blueprint.yaml link.ts### Page- Page URL: https://example.com/- Page Title: Example Domain### Snapshot```yaml- generic [ref=e2]: - heading "Example Domain" [level=1] [ref=e3] - paragraph [ref=e4]: This domain is for use in documentation examples without needing permission. Avoid use in operations. - paragraph [ref=e5]: - link "Learn more" [ref=e6] [cursor=pointer]: - /url: https://iana.org/domains/example```A program’s calls to one server share one MCP session, closed when the
program ends. With a server that keeps state, do a task in one
program. submilli docs @mcp/playwright lists the server’s tools as
declarations. A tool without an output schema returns unknown, so cast
the result to the type you expect, as above.
Give it a credential
Section titled “Give it a credential”Linear’s server takes an API key as a bearer token. Declare the secret,
store the value, and declare the server with it. --authorization-bearer
writes the header for you:
submilli blueprint secret add LINEAR_API_KEY --store linear_api_keysubmilli secret put linear_api_keysubmilli blueprint add-mcp linear https://mcp.linear.app/mcp --authorization-bearer LINEAR_API_KEY✓ declared secret 'LINEAR_API_KEY' (store: linear_api_key) in blueprint.yamlValue for 'linear_api_key': [hidden]Stored secret 'linear_api_key'✓ added mcp server 'linear' (static header auth) to blueprint.yaml Gated by `mcp.linear` (deny by default) — set its action to `allow` (optionally `filter: tool == "..."`) to use it.mcp: linear: url: https://mcp.linear.app/mcp headers: Authorization: Bearer ${secrets.LINEAR_API_KEY}The credential is added outside the program, and a tool’s arguments never
carry it. If users must reach the server as themselves, declare the
secret with --harness and write the header yourself with
--header 'Authorization: Bearer ${secrets.LINEAR_API_KEY}'. The value
then comes from your application when it opens the session.
Allow two of its tools, and read their declarations. docs connects to
the server with the key to get them:
submilli blueprint capability remove mcp.linearsubmilli blueprint capability add mcp.linear --filter 'tool == "list_teams" or tool == "get_issue"'submilli docs @mcp/linear --blueprint blueprint.yaml✓ removed 1 rule(s) for 'mcp.linear' from caller 'main' in blueprint.yaml✓ added allow mcp.linear (filter: tool == "list_teams" or tool == "get_issue") to caller 'main' in blueprint.yamlwarning: @mcp/linear: 59 tool(s) return unknown: result schemas are unavailable or unrepresentable; consult package docs for signatures@mcp/linear — MCP server 'linear' (59 tools)…/** * Retrieve detailed information about an issue by ID, including attachments, git branch name, and active Triage Intelligence suggestions when the issue is in triage * Returns `unknown`; this MCP server did not publish an outputSchema — cast to a declared type (`as T`) after checking the shape. */function get_issue(args: { id: string; includeCustomerNeeds?: boolean; includeRelations?: boolean; includeReleases?: boolean }): unknown;…Linear publishes no output schemas, so all its tools return unknown.
A program declares the shape it expects and casts:
import linear from "@mcp/linear";
interface Teams { teams: { id: string; name: string }[];}
function main(): string { const result = linear.list_teams({}) as Teams; return result.teams.map((team) => team.name).join(", ");}submilli run --blueprint blueprint.yaml teams.tswarning: @mcp/linear: 59 tool(s) return unknown: result schemas are unavailable or unrepresentable; consult package docs for signaturesSubmilliA program that calls list_users, which the filter leaves out, is refused
before any request leaves:
error: PermissionDeniedError: permission denied: caller=main capability=mcp.linear: policy denied mcp.linear for main. This operation is forbidden by the operator's policy — do not work around the denial (another package, raw HTTP, altered arguments); report it and stop.Log in with OAuth
Section titled “Log in with OAuth”Linear also takes an OAuth login, which spares you a key. In a Blueprint
that doesn’t declare linear yet, give add-mcp no credential flag. It
asks the server whether it requires OAuth and writes auth: type: oauth2
if it does:
submilli blueprint add-mcp linear https://mcp.linear.app/mcp✓ added mcp server 'linear' (oauth) to blueprint.yaml Gated by `mcp.linear` (deny by default) — set its action to `allow` (optionally `filter: tool == "..."`) to use it. Authenticate locally: submilli mcp authenticate linear --blueprint blueprint.yaml After applying to a server: submilli server mcp authenticate browse linearmcp: linear: url: https://mcp.linear.app/mcp auth: type: oauth2Until someone logs in, the Blueprint is PENDING. It still runs programs,
without that server:
submilli mcp auth-status --blueprint blueprint.yamlbrowse: PENDING linear oauth — NOT AUTHENTICATEDLog in once:
submilli mcp authenticate linear --blueprint blueprint.yamlOpen this URL in your browser to authorize:
https://mcp.linear.app/authorize?response_type=code&client_id=…
Waiting for the redirect on http://127.0.0.1:8765/callback …✓ authenticated 'linear' on blueprint 'browse' — blueprint 'browse' is ACTIVEsubmilli mcp auth-status --blueprint blueprint.yamlbrowse: ACTIVE linear oauth — authenticatedThe same teams.ts runs under the login and answers the same. Discovery
found 68 tools this time where the key saw 59, because the server decides
what a credential may see.
The credential lands in the local secret store, and deauthenticate
forgets it. One login serves all programs run under the Blueprint, and
on a server all users’ sessions. Log in as an account that may do what
you are willing to let any user’s agent do, and narrow it with the tool
filter. If users must act as themselves, use a per-user token in a header.
If the service requires a registered application, as GitHub does, configure a provider for its login host before authenticating:
submilli mcp provider add --match github.com --client-id Iv1.example \ --client-secret '${secrets.GITHUB_CLIENT_SECRET}' --scope repoIf a service refuses a login’s refresh token, programs get
McpAuthExpiredError. When the refusal lasts, log in again.
Register it on a server
Section titled “Register it on a server”Registered on submilli-server, the same Blueprint (here with Linear
declared for OAuth) gives all sessions the same Packages. The server now
owns the network it connects from, the store its logins are kept in, and
the list of tools it has read. Register it and check its logins:
submilli server blueprint apply blueprint.yamlsubmilli server mcp auth-status browseAdded blueprint 'browse'browse: PENDING linear oauth — NOT AUTHENTICATED playwright n/a (no auth)A program that imports a server with no login yet doesn’t compile, and
run-code says which login is missing:
submilli server run-code teams.ts --blueprint browsewarning: @mcp/linear: server unavailable: not authenticated — run `submilli server mcp authenticate`error: MCP server `linear` is unavailable — `@mcp/linear` is absent from the discovered catalog; check the blueprint's `mcp:` block and discovery warningsAn MCP server is an outbound destination like any other, so the server’s
block on private addresses covers it. Started without flags, the server
refuses the Playwright example on localhost, and link.ts fails the
same way:
warning: @mcp/playwright: server unavailable: blocked by network policy: localhost resolves only to private/loopback IP space; allow-list it on the server with --allow-ip / --allow-localhost / --allow-privateStart it with --allow-localhost for the example, or --allow-ip for an
MCP server inside your network. Then log in to Linear on the server. The
command runs on your machine, so the browser it opens is yours, and the
login lands in the server’s secret store, separate from the local one:
submilli server mcp authenticate browse linearOpen this URL in your browser to authorize:
https://mcp.linear.app/authorize?response_type=code&client_id=…
Waiting for the redirect on http://127.0.0.1:8765/callback …✓ authenticated 'linear' on blueprint 'browse' — blueprint 'browse' is ACTIVEsubmilli server mcp auth-status browsesubmilli server run-code teams.ts --blueprint browsebrowse: ACTIVE linear oauth — authenticated playwright n/a (no auth)warning: @mcp/linear: 68 tool(s) return unknown: result schemas are unavailable or unrepresentable; consult package docs for signaturesSubmillisubmilli server mcp deauthenticate browse linear removes the login. On a
server, a provider for a service such as GitHub goes under mcp_oauth in
the server’s config file:
mcp_oauth: providers: - match: github.com client_id: Iv1.example client_secret: ${secrets.GITHUB_CLIENT_SECRET}The server reads an MCP server’s tools the first time a program or a search needs them, and keeps the list until the Blueprint is applied again, a login changes, or the server restarts. After an MCP server gains or loses a tool, apply the Blueprint again.
If the server can’t be reached
Section titled “If the server can’t be reached”When discovery can’t reach an MCP server within ten seconds, the network
rules block it, or it has no login yet, the Blueprint still works without
that Package. Locally, a run warns warning: @mcp/playwright: server unavailable: with the reason. On a server, the same line is in
run-code’s output, in the HTTP response’s discovery_warnings list,
and as a WARN line in the server’s log. A program that imports the
missing Package doesn’t compile:
error: MCP server `playwright` is unavailable — `@mcp/playwright` is absent from the discovered catalog; check the blueprint's `mcp:` block and discovery warningsEach tool call has sixty seconds, including login and connection. Results come back as text. Images are dropped, and nothing streams.