Using LLMs to build a Crosswork Planning MCP server
Introduction
Crosswork Planning predicts the impact of network changes, traffic growth, new services, and potential failures. Operators usually perform these tasks in the Design application, which provides an interactive graphical interface for simulation, optimization, and analysis.
The same capabilities are available programmatically. The Optimization and Prediction Module (OPM) Python library connects to the DesignAPI service on a Crosswork Planning instance, opens a plan, and runs route simulation, failure impact, and Simulation Analysis on the server. The tutorial Using LLMs for Crosswork Planning script prototyping showed how an LLM coding agent can be leveraged to write a one-off script against this library.
This tutorial goes one step further. Instead of asking the agent for a script that answers a specific question, we ask it to build a Model Context Protocol (MCP) server that exposes Crosswork Planning capabilities as a set of tools that any MCP-capable LLM client can use. Once the server is connected, you may ask questions such as “What happens if the San Jose to Kansas City link fails?” in natural language, and the LLM decides which simulation to run and explains the results to the operator.
Note: Crosswork Planning is slated to ship with a fully supported MCP server in a future release. This tutorial shows how you can build and use your own MCP server with LLMs, using the Crosswork Planning Python library.
Building on these capabilities, Crosswork AI offers an Agent Development Kit (ADK) for developing and operating AI agents, enabling MCP-powered network planning applications. An illustrative use case is presented in the Cisco Community blog, From Static to Autonomous: The Evolution of Network Planning. Beyond agent development, the Crosswork AI framework brings together agent observability and tracing, agent evaluation, planning and orchestration services, governed model access, standardized tool integration, and knowledge-graph grounding. Together, these capabilities provide a structured foundation for AI-driven network operations that may not be available through general-purpose LLM interactions alone.
What you’ll build
In this tutorial, you will ask an LLM coding agent to build a single-file Python MCP server for Crosswork Planning, register it in Cursor, and use it to analyze a sample WAN plan.
When the server is connected, a conversation like this:
User: I have us_wan.txt in my workspace. Upload it and tell me what happens
if the SJC-KCY link fails. Also run simulation analysis on circuits.
produces an answer grounded in real simulation output from Crosswork Planning:
Agent: 1. upload_plan(us_wan.txt) → plan_ref upload:abc123, 33 nodes / 50 circuits / 95 demands
2. failure_sim(plan_ref=upload:abc123, node_a=cr1.sjc, node_b=cr1.kcy)
→ 10 reroutes; cr2.wdc:to_cr1.nyc oversubscribed at 113.39%
3. get_wc_traffic(plan_ref=upload:abc123, failure_sets=circuits)
→ top interface NYC-WDC 113.39% (scenario_count 29 when reported)
4. Summarize findings for the user
You don’t need to write the server yourself. The agent reads the Cisco SDK and writes the code; your job is to set up connectivity, describe the server clearly, and check the results against the Design application.
This takes about 90 minutes, most of it spent on one-time certificate setup.
How the MCP server works
Model Context Protocol (MCP) defines how AI clients discover and invoke tools (functions with JSON schemas) over a standard transport. An MCP server can also expose resources (read-only data, such as a plan summary) and prompts (workflow templates the LLM can pull into context). The LLM client decides when to call each tool based on the user’s question.
The server in this tutorial uses stdio transport, which requires it to run on the same Linux workstation as the LLM client (for example, Cursor). Cursor launches the server as a child process and exchanges JSON-RPC messages over stdin and stdout. No network port is opened on your workstation, and there is no separate “start the server” step for daily use. The server in turn connects to DesignAPI on the Crosswork Planning instance over mutual TLS (mTLS) on port 30744.
┌─────────────────────────────────────────────────────────────────┐
│ Linux Workstation (same host as Cursor) │
│ │
│ ┌─────────────┐ stdin/stdout ┌──────────────────────────┐ │
│ │ Cursor │ ◄──────────────► │ cp_sample_mcp.py │ │
│ │ MCP client │ JSON-RPC stream │ --transport stdio │ │
│ └─────────────┘ │.plan_staging/ │ │
│ │ CARIDEN_HOME=cw-planning│ │
│ └────────────┬─────────────┘ │
└─────────────────────────────────────────────────┼───────────────┘
│ mTLS:30744
▼
┌───────────────────────┐
│ Crosswork Planning VM │
│ designapid │
└───────────────────────┘
Figure 1: The MCP client spawns the server locally; the server runs simulations on DesignAPI
The LLM never reads plan files from the Crosswork Planning server. Instead, it reads the plan from your workspace and sends its content to the upload_plan tool. The server stages the file locally and returns a plan_id and canonical plan_ref (upload:<plan_id>). Pass that plan_ref to every later tool call.
The finished server exposes 14 tools, 5 resources, and 2 prompts:
| Category | Tools | What they do |
|---|---|---|
| Plan lifecycle | upload_plan, list_uploaded_plans, delete_uploaded_plan |
Stage a plan from the client and manage staged plans. |
| Catalog | get_plan_summary, list_circuits, list_demands, list_lsps |
Read nodes, circuits, demands, and label-switched paths (LSPs). |
| Route simulation | get_demand_path, get_igp_path, get_lsp_path |
Show how a demand, an Interior Gateway Protocol (IGP) shortest path, or an RSVP-TE tunnel or Segment Routing (SR) policy is routed. |
| What-if | failure_sim |
Fail a specific circuit and report reroutes and utilization changes. |
| Analysis | get_wc_traffic, get_traffic_growth |
Run Simulation Analysis for worst-case utilization, and forecast traffic growth. |
| Health | designapi_health_check |
Verify certificates, configuration, and DesignAPI reachability. |
Compared with other ways of automating Crosswork Planning:
| Approach | Strength | Consideration |
|---|---|---|
| Collector external scripts | Integrated into collection chains | Runs only during collection; file-path based |
| Job Manager script jobs | Scheduled, visible in the user interface | Each question needs its own script |
| MCP server (stdio) | Natural-language interface; immediate what-if analysis; no network listener | The MCP client and OPM SDK must run on a host that can reach DesignAPI |
| REST API (CP 8.0) | Platform-agnostic; does not require the SDK | Simulations must run as a Job Manager script or CLI job |
Before you begin
You need:
- Crosswork Planning 8.0 or later, with permission to manage OPM certificates. This tutorial was verified on 2026-10-08 against release 8.0 (Build 385).
- A Linux workstation with Python 3.11 or later, and TCP reachability to the Crosswork Planning instance on port 30744. The OPM SDK is Linux-oriented; a Linux lab VM or Windows Subsystem for Linux (WSL) is the simplest option.
- The Design/OPM SDK (
cp_design_opm_sdk.tgz) from your Crosswork Planning installation or the Cisco software portal. - An LLM coding agent and MCP client, such as Cursor or Claude Desktop. Other models and clients should work, but their output will differ.
- A sample plan file: this tutorial uses
us_wan.txt(33 nodes, 50 circuits, 95 demands), exported from the Design application user space.
The generation prompts and a reference copy of the server, cp_sample_mcp.py, are available on GitHub.
Keep sensitive data out of your prompts. Don’t paste passwords, tokens, private keys, customer plan files, or real topology data into an LLM. Use the sample plan while you learn, and keep certificates on disk where only the SDK reads them.
Step 1: Connect the OPM library to Crosswork Planning
The MCP server is only as useful as its connection to DesignAPI, so set that up first and confirm it works before involving the agent. This is a one-time task per workstation.
Start DesignAPI — obtain a JWT bearer token from Crosswork single sign-on, then check DesignAPI status through the OPM REST API on port 30603 and start it if needed. You may use scripts such as cw_get_jwt to obtain a JWT.
export TOKEN=<your-jwt-bearer-token>
export OPM="https://<cp-host>:30603/cp/opm-service/api/v1"
curl -sk -H "Authorization: Bearer $TOKEN" "$OPM/status" | python3 -m json.tool
curl -sk -X POST -H "Authorization: Bearer $TOKEN" "$OPM/start"
The JWT is used only to onboard certificates. All simulation traffic uses mTLS client certificates.
Extract the SDK — the SDK directory becomes CARIDEN_HOME:
mkdir -p ~/crosswork-mcp && cd ~/crosswork-mcp
tar xzf cp_design_opm_sdk.tgz
export CARIDEN_HOME=$PWD/cw-planning
ls "$CARIDEN_HOME/lib/python/com/cisco/wae/opm/network"
Create a CA-signed client certificate — Crosswork Planning 8.0 accepts only CA-signed client certificates during onboarding. The SDK’s generate_client_certs script produces self-signed certificates, which onboarding rejects with HTTP 400 or 422. In production, request a client authentication certificate from your enterprise PKI. In a lab, create a private CA with OpenSSL. First create openssl-ca.cnf:
[ req ]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_ca
[ dn ]
CN = Lab Test CA
O = Lab
C = US
[ v3_ca ]
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
Then create openssl-client.cnf:
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_client
[ dn ]
CN = lab-design-client
O = Lab
C = US
[ v3_client ]
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
Generate the CA and the client certificate, then verify them:
mkdir -p opm-certs && cd opm-certs
openssl genrsa -out ca_key.pem 4096
openssl req -x509 -new -nodes -key ca_key.pem -days 3650 \
-out test_ca.pem -config ../openssl-ca.cnf
openssl genrsa -out client_key.pem 2048
openssl req -new -key client_key.pem -out client.csr -config ../openssl-client.cnf
openssl x509 -req -in client.csr -CA test_ca.pem -CAkey ca_key.pem -CAcreateserial \
-out client_cert.pem -days 825 -sha256 -extensions v3_client -extfile ../openssl-client.cnf
openssl x509 -in test_ca.pem -noout -ext basicConstraints # CA:TRUE
openssl x509 -in client_cert.pem -noout -issuer -subject # issuer differs from subject
openssl verify -CAfile test_ca.pem client_cert.pem # OK
cd ..
Before using any certificate, inspect it with openssl x509 -text -noout -in <certificate_file> and confirm that it is within its validity dates, uses an RSA key of at least 2048 bits (or P-256 or stronger), and is signed with SHA-256 or stronger. A private lab CA is self-signed by design; use it only in a lab.
If you have already put the SDK on LD_LIBRARY_PATH, call OpenSSL with a clean library path. The libraries under cw-planning/lib make the system openssl CLI fail (undefined symbol: EVP_sm3):
env -u LD_LIBRARY_PATH openssl x509 -text -noout -in <certificate_file>
Onboard the certificate — upload the client certificate and its issuing CA through the OPM REST API. Expect HTTP 200 with {"message": "Certificates imported successfully"}:
curl -sk -X POST "$OPM/certs" \
-H "Authorization: Bearer $TOKEN" \
-F "cert=@opm-certs/client_cert.pem" \
-F "ca_cert=@opm-certs/test_ca.pem" \
-F "title=dev-vm-cert"
Install the certificates for the SDK — the SDK loads mTLS settings from fixed file names under $CARIDEN_HOME/etc/certs/. Copy the files there, append the DesignAPI server certificate to the trust bundle, and restart DesignAPI:
cp opm-certs/client_cert.pem $CARIDEN_HOME/etc/certs/designapi_user_cert.pem
cp opm-certs/client_key.pem $CARIDEN_HOME/etc/certs/designapi_user_key.pem
cp opm-certs/test_ca.pem $CARIDEN_HOME/etc/certs/ca_cert.pem
chmod 600 $CARIDEN_HOME/etc/certs/designapi_user_key.pem
curl -sk -H "Authorization: Bearer $TOKEN" "$OPM/certs/server" >> "$CARIDEN_HOME/etc/certs/ca_cert.pem"
curl -sk -X POST -H "Authorization: Bearer $TOKEN" "$OPM/stop"
curl -sk -X POST -H "Authorization: Bearer $TOKEN" "$OPM/start"
The private key never leaves your workstation; only the public certificate is uploaded.
Install the MCP dependencies — the server needs FastMCP and Pydantic:
pip install "fastmcp>=3.0.0" "pydantic>=2.0,<3"
# This tutorial was checked with FastMCP 3.4.5 and Pydantic 2.13.4.
Step 2: Ask the agent to build the MCP server
Now describe the server you want. As in the script prototyping tutorial, a good prompt reads like a requirements document. For an MCP server, it also has to carry facts the agent cannot reliably discover on its own, because some SDK docstrings don’t match the runtime behavior.
Open your agent in the ~/crosswork-mcp folder, where it can read the SDK under cw-planning/, and paste the minimal prompt from Appendix A. It begins like this:
You are a Python engineer familiar with FastMCP and Cisco Crosswork Planning.
Build a minimal, usable MCP server in one file. Keep the implementation small
and direct. Do not add features that are not listed here.
## Deliverable
Write one executable file, `cp_sample_mcp.py`, for Python 3.11 or newer.
...
Before writing Cisco calls, inspect the installed SDK under `CARIDEN_HOME` and
its local documentation. Use only APIs you can verify there...
If a required API is absent, do not invent a substitute and do not write the file.
Return `status: blocked` with the paths and symbols you checked.
The prompt is long, but each section has a job. Here is what the main parts mean in plain English:
| Section of the prompt | What it means |
|---|---|
Inspect the installed SDK under CARIDEN_HOME |
The agent reads the real Cisco library on disk instead of guessing from memory. |
Return status: blocked if a required API is absent |
The agent stops and tells you, rather than inventing a function that doesn’t exist. |
Runtime: re-exec with os.execv |
Native libraries are only loaded when the process starts, so the server restarts itself once with the right library path. You can then run it with plain python3. |
IceSSL loads mTLS from CARIDEN_HOME/etc/certs/ |
Certificates are read from the files you installed in Step 1, never from the source code or command line. |
HTTP must refuse to start without MCP_API_TOKEN |
The optional remote mode can’t be started without authentication. |
| Uploaded plans: list, get, and delete only the current owner’s plans | One client can’t see or delete another client’s plans. |
| Verified OPM/Design operational contract | A short list of exact SDK calls that are known to work on this release, including the cases where a docstring is misleading. |
| MCP surface: resources, prompts, tools | The full list of what the server must expose, with arguments and return values. |
| Finish: run live DesignAPI checks | If us_wan.txt is available, the agent must run the tools against the real instance and fix failures before it reports success. |
The operational contract is the most important part. These are lessons from real runs, written down so the agent doesn’t repeat them:
| Contract item | Why it is in the prompt |
|---|---|
open_plan(path, host=..., port=..., protocol="ssl") |
The open_plan docstring lists http and https, but the underlying connection manager accepts ssl or tcp. |
Reset with model.route_simulation = [], never None |
Assigning None leaves the failure list in a state where later failure calls raise AttributeError. |
Apply failures with model.route_simulation = [circuit,...] |
Setting circuit.failed = True is not the supported what-if mechanism. |
Pass Node objects to shortest_path, not name strings |
Passing a string raises an Ice ValueError and can affect later calls on the same connection. |
route.interfaces is unordered |
To show hop-by-hop paths, the server walks the topology from the source node instead of trusting set order. |
| Index Simulation Analysis results by model interface | sa.interfaces[iface].worst_case_utilization returns the vendor’s worst-case figure for each interface. |
create_growth_plans options are kebab-case |
GenericTool options such as "num-periods" are strings in the native library, not Python names, so the agent can’t discover them by reading Python code. |
The agent writes the server to your folder. Expect around 1,000 to 2,000 lines. You don’t need to read all of it, but skim the tool docstrings: they are what the LLM client reads when deciding which tool to call.
Note: A reference implementation,
cp_sample_mcp.py, is available on GitHub. Appendix A also producescp_sample_mcp.py; the steps below call out where generated copies may differ from the reference.
Step 3: Review how the server is put together
Whatever the agent generates, the server should follow the same shape as the reference implementation. Checking for these patterns is a quick way to review generated code without reading every line.
Bootstrap before importing Cisco modules — the OPM library must be on the Python and native library paths before any com.cisco.wae import, so the server sets up the environment first:
def _bootstrap_opm_env() -> Path:
cariden = _cariden_home()
if not (cariden / "lib" / "python").is_dir():
raise RuntimeError(
f"CARIDEN_HOME not found or invalid: {cariden}. "
"Set CARIDEN_HOME or place cw-planning/ next to this script."
)
os.environ["CARIDEN_HOME"] = str(cariden)
py_lib = str(cariden / "lib" / "python")
if py_lib not in sys.path:
sys.path.insert(0, py_lib)
#... PYTHONPATH and LD_LIBRARY_PATH updated here...
return cariden
_apply_env_overrides()
CARIDEN_HOME_PATH = _bootstrap_opm_env()
from com.cisco.wae.opm.network import open_plan # noqa: E402
from com.cisco.wae.opm.network.tools.simulation_analysis import SimulationAnalysis # noqa: E402
from fastmcp import FastMCP # noqa: E402
Open one plan per call, under a lock — the OPM objects are not thread-safe, so every tool opens a fresh copy of the plan inside a global lock. The staged upload is never modified:
@contextmanager
def _open_model(plan_path: Path) -> Iterator[Any]:
host, port, protocol = _cw_connection()
with _opm_lock:
with open_plan(str(plan_path), host, port, protocol) as network:
yield network.model
Return structured results — successful calls return {"ok": true,...}, and failures raise FastMCP’s ToolError with a short message and hint. An LLM parses structured JSON far more reliably than a stack trace.
Tell the LLM how to choose tools — the server’s instructions steer tool selection. For example, a worst-case question must go to Simulation Analysis, not to repeated failure simulations:
FastMCP(
"cw-planning-tutorial", # keep this unique in the MCP client
instructions=(
"Always upload_plan first when the user provides a plan file, "
"then pass the returned plan_id as plan_ref to simulation tools. "
"Tool routing: for worst-case traffic or Simulation Analysis "
"(e.g. 'worst case under circuit failures', 'most at-risk links'), "
"call get_wc_traffic only — do not call failure_sim. "
#...
),
auth=_build_auth_provider(),
middleware=_build_middleware(),
strict_input_validation=True,
)
Compare a baseline with a failure — the failure_sim tool runs the simulation twice in the same opened plan, once with no failures and once with the requested circuits failed, and then compares demand paths and interface traffic:
model.route_simulation = []
model.traffic_simulation = None
model.route_simulation.recompute()
before_paths = {str(d): _path_of(d) for d in model.demands if d.active}
before_ifaces = _iface_snapshot(model)
model.route_simulation = failed
model.traffic_simulation = None
model.route_simulation.recompute()
after_ifaces = _iface_snapshot(model)
rerouted, unrouted = _collect_link_failure_reroutes(model, before_paths)
Use the Cisco engine for worst-case analysis — get_wc_traffic calls SimulationAnalysis and reads the vendor’s worst-case figure for each interface. It never approximates worst case by looping over failure_sim:
sa = SimulationAnalysis(
model,
failure_types=list(args.failure_sets),
max_fail_per_int=args.max_fail_per_int,
)
sa.cache_valid = True
for iface in model.interfaces:
util = sa.interfaces[iface].worst_case_utilization
Keep stdout clean — in stdio mode, stdout carries the MCP protocol. All startup messages and audit logs go to stderr. A single stray print() to stdout breaks the connection with the client.
Step 4: Test the server without an MCP client
Before connecting Cursor, call the tools directly from Python. This separates DesignAPI problems from MCP client problems.
Place us_wan.txt in ~/crosswork-mcp, then run the following. Generated servers from the appendices read CW_DESIGNAPI_HOST and CW_DESIGNAPI_PORT. Some GitHub reference copies also accept CW_HOST / --cw-host; set both if you are unsure which file you are running.
Always pass the canonical plan_ref (upload:<plan_id>). A bare plan_id works on some reference copies, but a server generated from the minimal prompt treats an unmatched string as a filesystem path and fails with plan file not found.
cd ~/crosswork-mcp
export CARIDEN_HOME=$PWD/cw-planning
export CW_DESIGNAPI_HOST=<cp-host>
export CW_DESIGNAPI_PORT=30744
export CW_DESIGNAPI_PROTOCOL=ssl
# Optional alias used by some reference copies:
export CW_HOST=<cp-host>
python3 -c "
from pathlib import Path
import cp_sample_mcp as m # Appendix A/B deliverable or GitHub reference
plan = Path('us_wan.txt').read_text()
up = m.upload_plan('us_wan.txt', plan)
plan_ref = up.get('plan_ref') or ('upload:' + up['plan_id'])
print('upload nodes/circuits/demands:',
up.get('nodes') or up.get('node_count'),
up.get('circuits') or up.get('circuit_count'),
up.get('demands') or up.get('demand_count'))
print('health:', m.designapi_health_check(plan_ref=plan_ref))
print('failure:', m.failure_sim(
plan_ref=plan_ref, node_a='cr1.sjc', node_b='cr1.kcy'))
"
If upload_plan is not a module-level function (the full prompt may nest tools inside a register_all helper), call the same operations through an in-process FastMCP Client against the server object the file constructs.
Check that:
uploadreports 33 nodes, 50 circuits, and 95 demands (field names may benodesornode_count, and so on).healthisok: trueand DesignAPI is reachable. Generated servers nest this underreachability.reachableandcertificates; some reference copies use top-levelcerts_okanddesign_api_reachable.failurereportsrerouted_count: 10.
You may see IceSSL deprecation warnings and a urllib3 CiscoSSL notice. They don’t affect simulation results. A lab IceSSL private key that is group-readable (0664) should produce a warning, not a startup failure.
Next, confirm that the server starts in stdio mode. Use cp_sample_mcp.py, matching the file you generated:
python3 cp_sample_mcp.py --transport stdio
Stdout must stay empty: it carries the MCP protocol. Generated servers typically log one line to stderr (CARIDEN_HOME=... DesignAPI=<cp-host>:30744/ssl) and then wait. The GitHub reference prints a short banner such as:
Crosswork Planning MCP Server
CARIDEN_HOME: /home/user/crosswork-mcp/cw-planning
DesignAPI: <cp-host>:30744
Staging: /home/user/crosswork-mcp/.plan_staging
Transport: stdio
The process then waits for input. Press Ctrl+C to exit.
Step 5: Connect the server to Cursor
Add the server to your MCP settings, either in .cursor/mcp.json in your project or under Settings → MCP. Use absolute paths, and replace /home/user/crosswork-mcp with your folder.
Give the server a unique mcpServers key. Cursor namespaces tools by that key. If this workstation already has a Crosswork Planning MCP (cp_sample_mcp, cp-sample-mcp, or similar), do not reuse that key or you will collide with the existing tools. This tutorial uses cw-planning-tutorial. Keep the tool names (upload_plan, failure_sim, and so on) so the agent can follow the workflow below.
Generated servers from Appendix A accept --designapi-host. The GitHub reference and some Appendix B copies use --cw-host. Point args at the file you actually generated (cp_sample_mcp.py or cp_sample_mcp.py).
{
"mcpServers": {
"cw-planning-tutorial": {
"command": "python3",
"args": [
"/home/user/crosswork-mcp/cp_sample_mcp.py",
"--transport",
"stdio",
"--designapi-host",
"<cp-host>",
"--cariden-home",
"/home/user/crosswork-mcp/cw-planning"
],
"env": {
"CARIDEN_HOME": "/home/user/crosswork-mcp/cw-planning",
"CW_DESIGNAPI_HOST": "<cp-host>",
"CW_DESIGNAPI_PORT": "30744",
"CW_DESIGNAPI_PROTOCOL": "ssl",
"CW_HOST": "<cp-host>",
"PYTHONPATH": "/home/user/crosswork-mcp/cw-planning/lib/python",
"LD_LIBRARY_PATH": "/home/user/crosswork-mcp/cw-planning/lib:/home/user/crosswork-mcp/cw-planning/lib/python"
}
}
}
}
Reload MCP servers in Cursor. You don’t start the server yourself: Cursor launches it when it needs a tool. The MCP panel should list 14 tools, 2 prompts, and the health/upload resources. The three plan://{plan_id}/... resources are URI templates: some clients list all five, while an in-process FastMCP client lists only the two static URIs until a plan_id is known.
Claude Desktop uses the same command, args, and env entries in claude_desktop_config.json.
To confirm the connection, ask the agent:
Run designapi_health_check on the cw-planning-tutorial server.
A healthy server returns ok: true and a reachable DesignAPI. The exact JSON shape varies. A generated minimal server looks like this:
{
"ok": true,
"cariden_home": "/home/user/crosswork-mcp/cw-planning",
"designapi": {
"host": "<cp-host>",
"port": 30744,
"protocol": "ssl"
},
"certificates": {
"ca_present": true,
"client_cert_present": true,
"client_key_present": true,
"problems": []
},
"reachability": {
"reachable": true
}
}
Some reference copies flatten that into top-level certs_ok, design_api_reachable, crosswork_host, and design_api_port. Either shape is fine as long as DesignAPI is reachable and the certificate files are present.
Step 6: Ask planning questions
With the server connected, open us_wan.txt in your workspace and ask questions in plain English. You don’t need to name the tools; the LLM picks them from their descriptions. The following prompts were verified against us_wan.txt. Your agent’s wording will differ, but the numbers should match.
Simulate a link failure
Paste this prompt into your agent:
I have us_wan.txt in my workspace. Upload it and tell me what happens if the
link between cr1.sjc and cr1.kcy fails.
The agent uploads the plan, optionally lists circuits at sjc to find the link, and then calls failure_sim:
{
"tool": "failure_sim",
"arguments": {
"plan_ref": "upload:a1b2c3d4e5f6",
"node_a": "cr1.sjc",
"node_b": "cr1.kcy"
}
}
The response lists the failed circuits, the number of rerouted and unrouted demands, a sample of reroutes with before and after paths, the largest interface traffic changes, and any newly oversubscribed interfaces. Interface identifiers appear as either cr2.wdc:to_cr1.nyc or the SDK key if{cr2.wdc|to_cr1.nyc} — both name the same object. The key results, re-verified on 2026-10-08 against DesignAPI on release 8.0.0 (Build 385), are:
| Metric | Baseline | After SJC–KCY failure |
|---|---|---|
| Active demands | 95 | 95 |
| Rerouted demands | 0 | 10 |
| Unrouted demands | 0 | 0 |
cr1.sjc:to_cr1.kcy traffic |
978.6 Mbps | 0 |
cr2.wdc:to_cr1.nyc utilization |
85.6% | 113.39% |
cr1.nyc:to_cr2.wdc utilization |
— | 100.02% |
Traffic detours through the northern ring (San Jose, Chicago, New York, Washington DC) and oversubscribes the New York to Washington DC corridor.
To see a single demand’s normal path, follow up with:
Show me the baseline path for the demand from er1.sjc to er1.mia.
The agent calls get_demand_path and reports 6 hops, a path metric of 2169, and an average latency of 33.39 ms.
Find the most at-risk links
Paste this prompt into your agent:
Which interfaces are most at risk under any single circuit failure?
The agent calls get_wc_traffic with failure_sets: "circuits", which runs Simulation Analysis on DesignAPI. Expect this to take 10 to 30 seconds. The top interface is cr2.wdc:to_cr1.nyc (if{cr2.wdc|to_cr1.nyc}) at 113.39% worst-case utilization — the same link that the SJC–KCY failure oversubscribed. When the server exposes Cisco’s scenario list, scenario_count is 29 for this plan; generated minimal servers may omit that field and still return the same ranked utilizations.
Forecast traffic growth
Paste this prompt into your agent:
If demand grows 33% over one period, which links become oversubscribed?
The agent calls get_traffic_growth, which sets a growth rate on every active demand, runs the Create Growth Plans tool on DesignAPI, and compares utilization before and after. The New York to Washington DC circuit reaches 113.83%.
Note: Behavior of the
create_growth_planstool varies by build. On release 8.0.0 (Build 385) it registers a growth plan without updating demand traffic. The server detects this case, applies the compound growth formula ((1 + g/100)^{periods}) directly to demand traffic, and reportsfallback_used: trueorcompound_fallback_applied: true. Either flag means the numbers still come from the documented COMPOUND formula, not from a skipped forecast.
Combine questions
Because the LLM keeps context across the conversation, you can chain analyses into one capacity planning request:
Upload us_wan.txt, forecast 33% growth over one period, then run worst-case
analysis on circuit failures, and summarize which circuits are oversubscribed
or near capacity.
The server also provides this sequence as the capacity_planning_workflow prompt, and the link failure sequence as link_failure_workflow. In Cursor, these appear as reusable prompt templates.
Check the results in Design
The LLM’s summary is only as good as the simulation behind it, so confirm at least one result in the Design application:
| MCP tool | Design equivalent |
|---|---|
get_demand_path |
Demand routing in route simulation |
failure_sim |
Failure simulation of a circuit |
get_wc_traffic |
Simulation Analysis |
get_traffic_growth |
Create Growth Plans |
For the SJC–KCY scenario, open us_wan.txt in Design, fail the circuit between cr1.sjc and cr1.kcy, run route and traffic simulation, and confirm 10 reroutes and New York to Washington DC utilization of about 113%.
You’ve built and used your first Crosswork Planning MCP server.
Step 7: Fix a failed tool call with the agent
Generated servers don’t always work the first time. When a tool call fails, the fastest fix is usually to give the error back to the agent, together with what you expected.
One failure that the operational contract in the prompt guards against happens when the IGP shortest-path call receives node names instead of node objects:
ValueError: invalid value for argument 1 in operation getShortestPathRouteRecord
The shortest_path function needs OPM Node objects. A server that passes the string "cr1.sjc" gets this error from the Ice runtime, and later calls on the same pooled connection can fail too. A related mistake reports this error as a certificate or connectivity problem, which sends you to check certificates that are actually fine.
To fix a failure like this:
-
Copy the tool’s error response and any stderr lines around it.
-
Paste them into the agent with a short request, for example:
get_igp_path failed with the error below. Resolve source and destination with model.nodes[name] and pass those Node objects to shortest_path. Report SDK exceptions with their real exception class, not as certificate errors. Keep everything else the same. <paste the error here> -
Reload the MCP server in Cursor and ask the same question again.
When you find a new failure like this, add it to the operational contract in your prompt. The next time you or a colleague generate the server, the agent avoids the mistake from the start.
Step 8: Improve the prompt
The minimal prompt produces a server that works well for one person on one workstation. The full prompt in Appendix B produces the reference cp_sample_mcp.py design and adds the controls you need before sharing the server:
| Addition | Why it helps |
|---|---|
A prerequisite gate that returns status: blocked with evidence |
The agent proves every Cisco API it uses exists in the installed SDK before generating any code. |
| A compatibility-adapter section for version-sensitive calls | Release-specific SDK calls are kept in one place, which makes upgrades easier to review. |
| A SQLite plan registry with atomic, owner-scoped uploads | Uploads are never partially visible, and cleanup is safe while a simulation is running. |
| A bearer-token file mapping tokens to owners for HTTP | Each remote client gets its own identity and sees only its own plans. |
| Fail-closed certificate checks | The server refuses to start with expired certificates, weak keys, or SHA-1 signatures, and warns about self-signed certificates. |
| Hard caps on every list, path, and response size | A large plan can’t produce a response too big for the LLM’s context window. |
| A classified validation checklist (offline, SDK, live, deployment) | The agent reports which checks it actually ran and marks the rest as not run, rather than as passed. |
Try generating the server again with the full prompt, then repeat Steps 4 to 6. The simulation results should be the same.
What you learned
- Give the agent the real SDK. Pointing the agent at the installed library and its documentation keeps it grounded in APIs that exist in your release.
- Write down what the docs don’t say. An operational contract of verified calls prevents the agent from following a misleading docstring.
- Design tools for the LLM, not just for Python. Clear docstrings, server instructions, and structured
{"ok": true}responses decide whether the LLM picks the right tool and reads the result correctly. - Check every result in Design. The MCP server makes simulation conversational, but Crosswork Planning remains the authority on the numbers.
Before you use a generated server in production
A working sample session is a starting point, not proof that the server is safe to share. Before using it beyond your own workstation, make sure that:
- Someone who reads Python has reviewed the server, especially plan upload and file handling.
- The server passes a linter such as
pylintorruff. - The source contains no passwords, tokens, private keys, certificates, or deployment-specific addresses. Set the Crosswork host through
CW_DESIGNAPI_HOST(andCW_HOSTif your file still reads that alias) or the MCP clientenvblock. - Every certificate in
$CARIDEN_HOME/etc/certs/has been checked withopenssl x509 -text -noout, and the private key is mode0600. - You’ve tested bad input: a malformed plan, an oversized upload, a node pair with parallel circuits, and a node name that doesn’t exist.
- HTTP mode, if you use it, refuses to start without authentication, binds to
127.0.0.1by default, and sits behind a TLS reverse proxy when remote access is enabled. The reference implementation authenticates HTTP clients with a single sharedMCP_API_TOKEN; for a multi-user deployment, use a production token verifier with a token per owner, as described in the full prompt. - Uploaded plans are cleaned up, either with
delete_uploaded_planor by settingCW_PLAN_TTL_HOURS.
Next steps
- Extend the server: prove a new OPM capability in a Python session with
open_plan, add it as a@mcp.toolwith typed parameters and a clear docstring, test it by direct import as in Step 4, and then reload the server in Cursor. - Share the server: run it with
--transport httpon a shared host behind a TLS reverse proxy so several planners can use one installation. The README covers remote deployment and a sample systemd unit. - Learn more about the libraries: read the Crosswork Planning Design/OPM library package and authentication for the Design RPC and OPM Python library on Cisco DevNet.
- Learn more about MCP: read the Model Context Protocol specification and the FastMCP documentation.
- Script without MCP: for a single repeatable change to a plan, see Using LLMs for Crosswork Planning script prototyping and Using external scripts to manipulate network models in Crosswork Planning.
Appendix A: Minimal build prompt
This is the full minimal prompt from Step 2. It is also available as generation_prompt_simple.md.
Show the minimal prompt
# One-shot prompt: minimal Crosswork Planning MCP server
You are a Python engineer familiar with FastMCP and Cisco Crosswork Planning.
Build a minimal, usable MCP server in one file. Keep the implementation small
and direct. Do not add features that are not listed here.
## Deliverable
Write one executable file, `cp_sample_mcp.py`, for Python 3.11 or newer.
Name the FastMCP server `cw-planning-tutorial` so it does not collide with
other Crosswork MCP servers already registered in the client.
- Start with `#!/usr/bin/env python3` and a short module docstring that shows
how to run stdio and authenticated HTTP.
- Use `fastmcp.FastMCP`, Pydantic v2, and type hints.
- Return JSON-serializable `{"ok": True,...}` results from every tool.
- Raise FastMCP `ToolError` or `ResourceError` for expected failures. Include a
short hint and do not return stack traces, secrets, or certificate contents.
- Send logs and startup messages to stderr. Leave stdout for MCP stdio traffic.
- Do not emit TODOs, pseudocode, mocks, or extra modules.
Before writing Cisco calls, inspect the installed SDK under `CARIDEN_HOME` and
its local documentation. Use only APIs you can verify there, especially
`open_plan`, route and traffic simulation, LSP and segment-list access,
`SimulationAnalysis`, and the Design RPC `create_growth_plans` tool. If a
required API is absent, do not invent a substitute and do not write the file.
Return `status: blocked` with the paths and symbols you checked. If an API
exists but DesignAPI is unreachable or unlicensed at runtime, keep the tool and
return a clear capability error.
The Python docstrings are incomplete and sometimes wrong. Treat the
**Verified OPM/Design operational contract** section below as mandatory
ground truth for this release even when it contradicts an `open_plan`
docstring. GenericTool JSON option names are not Python symbols; they are
still required and must use the kebab-case names listed there (also present
as strings in `libdesignservice.so`). Do not invent camelCase tool options.
## Runtime
Resolve `CARIDEN_HOME` from `--cariden-home`, then the environment, then a
local `cw-planning` directory. Require `<CARIDEN_HOME>/lib/python`, add it to
`sys.path`, and put both `<CARIDEN_HOME>/lib` and `<CARIDEN_HOME>/lib/python`
on `LD_LIBRARY_PATH`. Setting `os.environ["LD_LIBRARY_PATH"]` in an already
running process does **not** load `libIceDiscovery.so`. After updating native
paths, if `import Ice` / `IcePy` would fail, re-exec the same interpreter
with `os.execv` (preserving argv and the new env) **before** any Cisco
import. Fail startup with an actionable message when `lib/python` is missing.
Read configuration from the environment:
- `CW_DESIGNAPI_HOST`, `CW_DESIGNAPI_PORT` (default `30744`),
`CW_DESIGNAPI_PROTOCOL` (default `ssl`), `CW_DESIGNAPI_TIMEOUT_S`
- `CW_DESIGNAPI_CA_FILE`, `CW_DESIGNAPI_CLIENT_CERT_FILE`,
`CW_DESIGNAPI_CLIENT_KEY_FILE`
- `CW_DESIGNAPI_USERNAME` and `CW_DESIGNAPI_PASSWORD_FILE`, or
`CW_DESIGNAPI_TOKEN_FILE`, only when the verified SDK requires them
- `CW_DEFAULT_PLAN`, `CW_PLAN_ROOT`, `CW_STAGING_DIR`, `CW_MAX_PLAN_BYTES`
- `MCP_HOST` (default `127.0.0.1`), `MCP_PORT`, `MCP_PATH`, `MCP_API_TOKEN`
Never put secrets, certificates, or private keys in source code or CLI
arguments. Use the SDK’s documented connection settings. Do not disable TLS
verification.
`main()` accepts `--transport {stdio,http}` (default `stdio`), `--host`,
`--port`, `--path`, `--allow-remote`, `--designapi-host`, and `--cariden-home`.
HTTP must refuse to start without `MCP_API_TOKEN`. Compare that token in
constant time. Binding outside loopback requires `--allow-remote`; document
that remote use belongs behind TLS. Stdio uses one fixed local owner. HTTP
uses one owner derived from the configured token.
IceSSL (protocol `ssl`) loads mTLS exclusively from
`CARIDEN_HOME/etc/certs/{ca_cert.pem,designapi_user_cert.pem,designapi_user_key.pem}`
via `ServiceConnectionManager`. Do not pass PEM contents into `open_plan`.
`ServiceConnectionManager.newServiceConnection(host, port, protocol)` accepts
`ssl` or `tcp`, not the `open_plan` docstring values `http`/`https`. Default
`CW_DESIGNAPI_PROTOCOL` is `ssl`.
Check configured certificate files for presence without printing their
contents. If the IceSSL private key exists and is owner-readable but not
mode `0600`/`0400`, warn on stderr and continue; refuse only if it is
missing, unreadable, or world-writable. Tell operators to inspect each
public certificate with:
```bash
openssl x509 -text -noout -in <certificate_file>
```
They must check validity dates, RSA 2048-bit or P-256-or-stronger keys, a SHA-2
signature, and whether a self-signed certificate is intentional and limited to
private use. If `LD_LIBRARY_PATH` includes the SDK `lib` directory, invoke
openssl as `env -u LD_LIBRARY_PATH openssl...` — Cisco SSL libraries break
the system openssl CLI (`undefined symbol: EVP_sm3`).
## Uploaded plans
Keep a JSON index in the staging directory, guarded by a lock. Store plan ID,
safe filename, path, size, SHA-256 digest, upload time, and owner. Accept UTF-8
text or strict base64, enforce the size limit, and allow only safe filename
characters. Generate plan IDs on the server. Large uploads and deletions return
a preview until the caller retries with `confirm=true`.
References use `upload:<plan_id>`. An owned bare `plan_id` must resolve the
same way; do not treat a matching upload id as a filesystem path. List, get,
and delete only the current owner’s plans, using the same not-found response
for missing and unauthorized IDs. A configured default plan is available only to the local stdio owner and
must stay inside `CW_PLAN_ROOT`. Reject path traversal and symlinks. HTTP
callers cannot pass filesystem paths.
Serialize all SDK work with one lock. Open a fresh plan context per operation
and do not modify the stored upload. Do not wrap every SDK exception as a
certificate/DesignAPI connectivity error. Include the real exception class and
a short message (never a stack trace). Ice `ValueError` from a bad argument
is a programming/API-usage error, not a missing cert.
## Verified OPM/Design operational contract
Use these patterns. They are required for a functional server on DesignAPI
IceSSL.
**Open and simulate**
```python
with open_plan(path, host=host, port=port, protocol="ssl", linger=False) as network:
model = network.model
model.route_simulation = [] # empty failure set; never assign None
model.traffic_simulation = None
model.route_simulation.recompute()
```
Assigning `model.route_simulation = None` leaves `_failed_objects` as a list.
Then `circuit.failed = True` crashes (`list` has no `.add`). Do not use
`.failed = True` for what-if. Apply failures with:
```python
model.route_simulation = [circuit, ...] # OPM circuit objects
model.route_simulation.recompute()
_ = model.traffic_simulation # trigger traffic sim
```
**IGP / shortest path — Node objects, not name strings**
`RouteSimulation.shortest_path` requires OPM `Node` objects. Passing a
string makes Ice raise `ValueError: invalid value for argument 1 in
operation getShortestPathRouteRecord` and can poison the pooled Ice
connection for later tools.
```python
src = model.nodes[source_name]
dst = model.nodes[destination_name]
route = model.route_simulation.shortest_path(src, dst, "igp") # or te/latency/bgp
```
**Order hops from topology, not from a Python set**
`route.interfaces` is unordered. Reconstruct hop order by walking from the
source node: pick the interface whose `node.name` is the current node, then
advance to `opposite_interface.node` (or the other circuit endpoint). Use
the same walk for IGP fill between node SIDs and from the last SID to the
destination. Return that walk as `resolved_ordered_path` (list of
`"{node}:{interface}"` strings). Do not skip IGP fill just because the SDK
collection is unordered.
**Circuit identity**
Accept all of: unique `node_a`+`node_b` (reject if parallel circuits), the
SDK key `ckt{n1|if1|n2|if2}`, and the shorthand `n1-n2` / `n2-n1`.
**Worst-case Simulation Analysis**
Index by **model** interface, not by iterating SA wrapper objects as if they
were model interfaces:
```python
sa = SimulationAnalysis(model, failure_types=["circuits"], max_fail_per_int=10)
sa.cache_valid = True
for iface in model.interfaces:
rec = sa.interfaces[iface]
util = rec.worst_case_utilization # e.g. 113.39
```
Return a stable `interface_id` such as `if{node|name}` or `{node}:{name}`,
never a Python `repr` of a wrapper object.
**Traffic growth (`create_growth_plans`)**
JSON options are kebab-case. Use the DesignAPI filesystem round-trip:
1. Set `demand.growth_percent` on active demands.
2. `network.rpc_plan.serializeToFileSystem("/tmp/mcp_growth_<unique>.txt")`
on the DesignAPI host (a unique path each call).
3. `newPlanFromFileSystem` that path; `newGenericTool().runTool(net,
"create_growth_plans", json.dumps({...}))` with:
```json
{
"plan-file": "<same server path>",
"create-plans-from": "DEMANDS",
"num-periods": 1,
"period-inc": 1,
"growth-method": "COMPOUND",
"run-sim-analysis": false
}
```
4. If the tool **raises** or succeeds without changing demand traffic, apply
compound `(1 + growth/100) ** (period_inc * num_periods)` through the
documented demand-traffic API (`Demand.traffic` or `DemandTrafficKey`) and
set `fallback_used: true`. Do not treat a tool exception as a hard failure
when the fallback can still produce a correct COMPOUND result.
5. Delete local and server-side temp plans in `finally`.
6. Reject `SIMPLE` unless you implement its documented linear math; never
label compound arithmetic as SIMPLE.
## MCP surface
Server instructions must say: upload a plan before simulation; use resources
for unfiltered reads; call `failure_sim` only for a known circuit failure; call
`get_wc_traffic` alone for worst-case analysis; call `list_lsps` before
`get_lsp_path`; use `resolved_ordered_path` for ordered LSP hops because the
route-simulation interface set is unordered.
Resources, all JSON:
1. `server://health` — SDK path, DesignAPI endpoint, certificate-file presence, and reachability.
2. `plan://uploads` — current owner’s plans.
3. `plan://{plan_id}/summary` — node, circuit, demand, and LSP counts, including active counts.
4. `plan://{plan_id}/circuits` — bounded active circuits.
5. `plan://{plan_id}/demands` — bounded active demands.
Prompts:
1. `link_failure_workflow(node_a, node_b)` — upload, inspect circuits, run `failure_sim`, review reroutes and new oversubscriptions, then inspect affected demand paths.
2. `capacity_planning_workflow(growth_percent=33.0)` — upload, read the summary, run growth, run circuit worst-case analysis, and summarize oversubscribed and near-capacity circuits.
Tools:
1. `upload_plan(name, content, encoding="text", confirm=False)` — store the plan, open it with OPM, and return its ID, `upload:` reference, metadata, and model counts. Delete the staged files if opening fails.
2. `list_uploaded_plans()`.
3. `delete_uploaded_plan(plan_id, confirm=False)`.
4. `designapi_health_check(plan_ref=None)`.
5. `get_plan_summary(plan_ref=None)`.
6. `list_circuits(plan_ref=None, node_filter=None, active_only=True, limit=100)` — endpoints, interfaces, capacity, and active state. Cap `limit` and report truncation.
7. `list_demands(plan_ref=None, source=None, destination=None, active_only=True, limit=100)` — name, endpoints, service class, traffic, and active state.
8. `get_demand_path(source, destination, plan_ref=None, demand_name=None, service_)` — clear failures, recompute the baseline, and return routed state, traffic, metric, latency, ECMP percentage, interfaces, traffic shares, and hop count.
9. `get_igp_path(source, destination, plan_ref=None, metric="igp")` — allow only metrics supported by the installed SDK. Resolve endpoints with `model.nodes[name]` and call `shortest_path` with those Node objects. Return interfaces and hop count.
10. `list_lsps(plan_ref=None, source=None, destination=None, active_only=True, limit=100)` — type, setup bandwidth, color, state, and path options.
11. `get_lsp_path(source, plan_ref=None, lsp_name=None, destination=None, color=None, destination_ip=None, path_option=None)` — resolve one RSVP-TE LSP by source and name, or one SR policy by source, destination, and color. Recompute the baseline. Return route metrics and these separate fields: configured `segment_list_hops`, ordered `resolved_ordered_path` as `"{node}:{iface}"` strings (interface SID directly; node SID filled by IGP using Node-object `shortest_path` plus the topology walk; then IGP fill to the destination), unordered `interface_set`, and comparison-only `reference_igp_shortest_path` (also Node-object `shortest_path`). Warn on unresolved segment steps. Do not present an unordered set as hop order; do not omit IGP fill because the SDK collection is unordered.
12. `failure_sim(plan_ref=None, node_a=None, node_b=None, failed_circuits=None, top_n=25, min_traffic_delta_mbps=1.0, sample_reroutes=5)` — fail one uniquely matched circuit between two nodes, or the listed circuits (`ckt{...}` or `nodeA-nodeB`). Capture the baseline, apply failures with `model.route_simulation = [circuits]`, recompute, and return failed IDs, rerouted and unrouted counts, a bounded reroute sample, the largest interface traffic changes, and new oversubscriptions. This is a specific what-if, not worst-case analysis.
13. `get_wc_traffic(plan_ref=None, failure_sets="nodes,circuits", max_fail_per_int=10, limit=15)` — validate failure-set names, run Cisco `SimulationAnalysis`, index `sa.interfaces[model_iface]`, and return the top interfaces by vendor worst-case utilization with stable interface ids. Do not loop over `failure_sim`.
14. `get_traffic_growth(plan_ref=None, growth_percent=33.0, num_periods=1, period_inc=1, growth_method="COMPOUND", threshold=100.0, near_capacity=80.0)` — record baseline circuit utilization, run kebab-case `create_growth_plans` via the filesystem round-trip above, and report oversubscribed and near-capacity circuits. If that tool raises or leaves traffic unchanged, apply the compound multiplier through the documented demand-traffic API and set `fallback_used`. Reject `SIMPLE` unless its documented linear math is implemented. Clean up every temporary file and temporary Design plan on both success and failure.
Reset simulation state before each baseline and recompute after failures. Treat missing utilization as unknown. Compare both sides of a circuit and report the higher-utilization side. Cap list and sample sizes.
## Finish
If a required API was missing, return only the blocked report. Otherwise compile the file and confirm that all 5 resource URIs, 2 prompts, and 14 tools are registered, that stdout is unused for logging, and that the source contains no secrets or certificate material. Confirm `os.execv` (or equivalent) is used so `python3 cp_sample_mcp.py` works without a pre-exported `LD_LIBRARY_PATH`. Run any small offline checks you can.
If `CW_DESIGNAPI_HOST` is set and `us_wan.txt` is next to the script, you **must** run live DesignAPI checks before finishing: upload the plan; `get_demand_path` er1.sjc→er1.mia (expect hop_count 6, metric ~2169); `get_igp_path` cr1.sjc→cr1.kcy (non-empty, no Ice ValueError); `failure_sim` node pair cr1.sjc/cr1.kcy (rerouted_count 10); `failure_sim` `failed_circuits=["cr1.sjc-cr1.kcy"]`; `get_wc_traffic` circuits (top util ~113, human-readable interface ids); `get_traffic_growth` 33% one period (`ok` true, either tool growth or `fallback_used`). Fix failures before answering. State any live checks that still could not run.
Appendix B: Full build prompt
This is the full prompt from Step 8. It is also available as generation_prompt.md.
Show the full prompt
# One-shot build prompt: Crosswork Planning MCP server
You are a senior Python platform engineer with deep experience in the Model
Context Protocol (MCP), FastMCP, Cisco Crosswork Planning, the OPM Python API,
the Design RPC API, secure service design, and Pydantic v2.
Build a production-quality Crosswork Planning simulation MCP server. The result
must be complete, runnable, secure by default, and useful to an MCP client
without manual code completion.
## Non-negotiable deliverable
Create exactly one implementation file named `cp_sample_mcp.py`.
Name the FastMCP server uniquely (`cw-planning-tutorial` unless that name is
already taken in the MCP client). Do not reuse `cp_sample_mcp` or
`cp-sample-sim` if another server already uses them.
- Target Python 3.11 or newer.
- Use `fastmcp.FastMCP` and Pydantic v2.
- Include `#!/usr/bin/env python3`, a concise module docstring, local stdio and
authenticated HTTP launch examples, dependency expectations, and all
environment-variable names.
- Use type hints throughout and Ruff/Black-compatible formatting.
- Do not emit pseudocode, TODOs, ellipses, mock production behavior, omitted
handlers, or references to code outside the file.
- Do not create supporting Python modules. Runtime state files and temporary
plan files are allowed.
- If operating in a coding workspace, write the file and validate it there. If
only text output is possible, return the entire source in one fenced block.
This build has one prerequisite gate: the installed FastMCP and Cisco SDK must
expose enough documented API to implement all 14 tools without invented calls.
If that gate fails, do not create a partial or deceptive server. Return a
concise `status: blocked` evidence report naming the paths, package versions,
documentation, and required symbols checked. A capability that is present in
the SDK but unavailable at runtime because of licensing, credentials, or a
temporarily unreachable DesignAPI does not block generation; keep its tool
registered and return a safe `CAPABILITY_UNAVAILABLE` error at runtime.
## Establish API ground truth before coding
The Cisco APIs are release-sensitive. Inspect the installed SDK, its Python
source/type stubs, bundled examples, and locally available official
documentation before writing integration code. Training-memory guesses are not
acceptable evidence.
1. Determine `CARIDEN_HOME` in this precedence order: CLI override,
environment variable, then a configurable local default.
2. Apply CLI overrides before importing any Cisco package.
3. Verify `<CARIDEN_HOME>/lib/python`, add it to `sys.path`, and put both
`<CARIDEN_HOME>/lib` and `<CARIDEN_HOME>/lib/python` on `LD_LIBRARY_PATH`.
Mutating `os.environ["LD_LIBRARY_PATH"]` in an already-started process does
not load `libIceDiscovery.so`. After updating native paths, if `Ice`/`IcePy`
cannot import, re-exec the same interpreter with `os.execv` (preserving argv
and the new env) **before** any Cisco import. Then inspect the installed
symbols.
4. Verify every imported Cisco class and every constructor, attribute, method,
enum, RPC option, and return type used by the implementation.
5. At minimum, establish whether this release provides and how it uses:
`open_plan`, `SimulationAnalysis`, `PlanKey`, `PlanFormat`,
`DemandTrafficKey`, the Design plan manager, the Design generic-tool
manager, plan serialization, route simulation, traffic simulation, LSP
keys, LSP paths, and segment lists.
6. Keep all version-sensitive Cisco operations in a small, clearly marked
compatibility-adapter section. Comments may state the installed versions
and the verified alternatives, but must not claim support that was not
checked.
7. Never invent an API or silently replace a missing Cisco analysis with a
home-grown approximation. Apply the prerequisite gate above when an API is
absent; use a runtime capability error only when the API exists but the
deployed service cannot currently execute it.
8. Python docstrings in this SDK are incomplete and sometimes wrong.
`open_plan`'s protocol text (`http`/`https`) does not match
`ServiceConnectionManager.newServiceConnection`, which accepts `ssl` or
`tcp`. Remote IceSSL DesignAPI uses `protocol="ssl"`. GenericTool JSON
option names are not Python symbols; they appear as kebab-case strings in
`libdesignservice.so`. Use the **Verified OPM/Design operational contract**
below as mandatory ground truth for those items. Do not invent camelCase
GenericTool options.
Use FastMCP APIs from one installed distribution/version only. Verify decorator,
middleware, authentication, lifespan, context, transport, and `run()` signatures
instead of mixing examples from different FastMCP releases.
## Server purpose and client instructions
Create one FastMCP server for uploading a Crosswork Planning plan and performing
topology inspection, baseline route inspection, LSP/SR-policy path inspection,
deterministic circuit-failure simulation, worst-case Simulation Analysis, and
traffic-growth forecasting.
The server instructions must tell clients to:
- upload a user-supplied plan first and pass the returned canonical plan
reference to later operations;
- prefer MCP resources for unfiltered, read-only data;
- use `failure_sim` only for a specifically identified circuit/link failure;
- use `get_wc_traffic` alone for worst-case or Simulation Analysis questions;
- call `list_lsps` before `get_lsp_path`;
- treat a route-simulation interface collection as unordered unless the SDK
explicitly guarantees order, and use resolved segment-list expansion for
ordered LSP/SR-policy hops.
## Configuration and startup
Implement one typed central configuration model. Reject malformed values and
unknown keys only within the application's own config namespace; ignore
unrelated process environment variables. Support these environment variables:
- `CARIDEN_HOME`;
- `CW_DESIGNAPI_HOST`, `CW_DESIGNAPI_PORT`, `CW_DESIGNAPI_PROTOCOL`,
`CW_DESIGNAPI_TIMEOUT_S`, `CW_DESIGNAPI_CA_FILE`,
`CW_DESIGNAPI_CLIENT_CERT_FILE`, `CW_DESIGNAPI_CLIENT_KEY_FILE`, and
`CW_DESIGNAPI_SERVER_NAME`;
- `CW_DESIGNAPI_USERNAME`, `CW_DESIGNAPI_PASSWORD_FILE`, and
`CW_DESIGNAPI_TOKEN_FILE` when the verified SDK uses password or token
authentication rather than mTLS; secret files must have private permissions,
and mutually exclusive authentication modes must be rejected;
- `CW_DEFAULT_PLAN`, `CW_PLAN_ROOT`, `CW_STAGING_DIR`,
`CW_MAX_PLAN_BYTES`, `CW_LARGE_UPLOAD_CONFIRM_BYTES`,
`CW_OWNER_QUOTA_BYTES`, and `CW_PLAN_TTL_HOURS`;
- `MCP_HOST`, `MCP_PORT`, `MCP_PATH`, `MCP_RATE_LIMIT_RPS`, and
`MCP_RATE_LIMIT_BURST`;
- `MCP_TOKEN_FILE`, a root/user-readable-only JSON secret file mapping opaque
bearer tokens to stable owner IDs.
If the installed Cisco release obtains DesignAPI mTLS settings exclusively from
its own documented configuration, verify and use that mechanism instead of
inventing constructor arguments. This release loads IceSSL from
`CARIDEN_HOME/etc/certs/{ca_cert.pem,designapi_user_cert.pem,designapi_user_key.pem}`
inside `ServiceConnectionManager` (`IceSSL.VerifyPeer=2`). Environment values
above may point to trust and identity files but must never contain private-key
or certificate contents. Do not accept secrets through CLI arguments. Default
`CW_DESIGNAPI_PORT` is `30744`. Default `CW_DESIGNAPI_PROTOCOL` is `ssl`.
Also accept `--designapi-host` as an alias of `--cw-host`.
Use safe defaults:
- stdio transport;
- HTTP bound to `127.0.0.1`;
- verified TLS for DesignAPI;
- no deployment-specific Crosswork address;
- private filesystem permissions;
- no embedded password, token, private key, certificate, secret, or credentialed
connection string.
Provide `main()` with at least:
- `--transport {stdio,http}`, default `stdio`;
- `--host`, `--port`, and `--path`;
- `--allow-remote`;
- `--cw-host` and `--designapi-host` (aliases for the DesignAPI host);
- `--cariden-home`;
- a non-network `--check` or equivalent startup diagnostic mode.
Parse and apply arguments that affect SDK discovery before SDK imports. Startup
must validate configuration and required capabilities, reconcile/purge stale
uploads, and print only sanitized diagnostics to stderr. Stdio stdout must
remain exclusively available to the MCP protocol.
HTTP transport must fail closed unless `MCP_TOKEN_FILE` is configured, has
private permissions, contains at least one valid token-to-owner mapping, and is
handled by a production-capable verifier supported by the installed FastMCP
release. Accept tokens only through the Authorization Bearer scheme. Compare
candidate tokens in constant time and derive the stable owner ID only from the
matched server-side mapping, never from tool arguments. Do not log or echo the
mapping. A shared token intentionally represents one owner; different owners
must have different tokens. For stdio, use one explicit constant local
principal for that server process.
Require an explicit flag for non-loopback binding, enable host/origin
protection, apply per-owner rate limiting, and configure a concrete
request-body limit at the HTTP server or documented reverse-proxy boundary.
Non-loopback deployment must refuse to start unless direct TLS is configured
through supported FastMCP settings or an explicit trusted-reverse-proxy mode is
enabled. Do not trust forwarded identity headers. Do not use a debug
authentication provider in production.
## Plan registry and filesystem safety
Implement a persistent SQLite uploaded-plan registry under the staging
directory. Use transactions, foreign-key enforcement, busy timeouts, unique
constraints, and active-operation leases so publication, lookup, quota
accounting, deletion, and TTL cleanup are race-safe. Run one server process;
reject configurations that request multiple workers unless equivalent
cross-process vendor-operation coordination is implemented.
For every plan record store:
- server-generated unguessable plan ID;
- safe display filename;
- canonical path managed by the server;
- exact byte size and SHA-256 content digest;
- upload timestamp and expiry;
- input encoding;
- authenticated owner ID;
- validation state.
Required behavior:
- accept UTF-8 text and strict validated base64;
- validate decoded size, not encoded size;
- allow only an explicitly defined safe filename and plan-ID character set;
- reject path separators, traversal, NULs, control characters, symlinks,
non-regular files, and collisions;
- reject impossible encoded lengths before decoding, strictly decode with a
hard decoded-size cap, enforce the HTTP request-body limit above, and do not
falsely claim handler-level decoding prevents the MCP framework from
buffering its request;
- require an explicit `confirm=true` retry before a large upload;
- write privately to a unique temporary file, validate exact size/digest,
fsync, then atomically publish;
- never expose a partial or failed upload;
- perform owner-scoped lookup, listing, deletion, and TTL cleanup without
revealing whether another owner's ID exists;
- require explicit confirmation before deletion;
- make deletion and cleanup race-safe with active simulations;
- recover or safely remove stale temporary state after restart;
- never follow symlinks during cleanup.
Canonical uploaded references must use `upload:<plan_id>`; an authorized bare ID
may also be accepted. The optional `plan_id` argument may provide a custom ID
only for the trusted stdio principal; HTTP always receives a server-generated
ID. A collision must return the same generic response as any unusable ID so it
cannot reveal another owner's record. Never reuse a deleted ID.
A configured default plan is optional and available only to the trusted stdio
principal. HTTP operations must supply an owned upload reference.
Direct local filesystem references use an explicit `file:<path>` syntax and are
allowed only for the trusted local stdio principal. Open them relative to an
allowlisted root using `openat`/`O_NOFOLLOW` or an equivalent race-safe
descriptor-based traversal, verify the opened descriptor with `fstat`, and copy
it into managed storage before use. Reject traversal, symlinks, hard links,
non-regular files, and path-replacement races. HTTP callers must never access
arbitrary server paths. Do not include sensitive absolute paths in remote
responses.
## Concurrency, isolation, errors, and responses
Assume OPM and Design RPC objects are blocking and not thread-safe. Serialize
vendor operations with a lock and use one HTTP worker unless cross-process
coordination is implemented. Acquire an active-operation lease before opening a
private working copy of a plan; deletion and expiry must wait for or safely
reject active leases. Keep each simulation inside a newly opened working
context. Never give the SDK write access to the registered baseline artifact.
Use bounded inputs and outputs. Configure vendor-supported RPC/network
timeouts. Do not describe `Future.result(timeout=...)` or async cancellation as
stopping an in-flight native SDK call. If the SDK provides no cancellation,
report a soft caller timeout while retaining the operation lease and lock until
the worker really completes; use a killable subprocess only if SDK licensing
and object transfer are verified to support it. If blocking work is invoked
from an async handler, offload it without violating SDK thread affinity.
Return JSON-serializable dictionaries with a consistent success envelope:
```python
{"ok": True, ...}
```
Map expected validation, lookup, SDK, filesystem, timeout, and authorization
failures to concise FastMCP `ToolError` or `ResourceError` responses with stable
safe error codes and actionable hints. Do not leak stack traces, credentials,
certificate contents, another owner's metadata, or sensitive paths. Avoid
blanket `except Exception` except at a boundary that records a safe error and
immediately re-raises or maps it.
Add structured audit logging for tools, resources, and prompts. Include target,
authenticated owner/client identifier, correlation ID, duration, result, and
exception class. Never log plan content, bearer tokens, credentials, or
certificate contents.
## MCP resources
Register these read-only JSON resources:
1. `server://health`
- sanitized SDK/runtime version and capability status;
- sanitized DesignAPI endpoint;
- required certificate-file presence;
- DesignAPI reachability when a plan can be opened;
- safe error code and actionable hint.
2. `plan://uploads`
- only plans owned by the current principal.
3. `plan://{plan_id}/summary`
- node, circuit, active-circuit, demand, active-demand, LSP, active-LSP, and
LSP-path counts.
4. `plan://{plan_id}/circuits`
- bounded active-circuit catalog.
5. `plan://{plan_id}/demands`
- bounded active-demand catalog.
Resource URI IDs must receive the same character validation and ownership
authorization as tool inputs.
## MCP prompts
Register:
1. `link_failure_workflow(node_a, node_b)`
- upload if needed;
- inspect circuits;
- run the explicit failure simulation;
- review reroutes and new oversubscriptions;
- inspect affected demand paths.
2. `capacity_planning_workflow(growth_percent=33.0)`
- upload if needed;
- inspect the baseline summary;
- run traffic growth;
- run worst-case circuit-failure Simulation Analysis;
- summarize oversubscribed and near-capacity circuits.
## Verified OPM/Design operational contract
These patterns are mandatory. Do not replace them with docstring guesses.
**Native libs.** After setting `LD_LIBRARY_PATH` to
`<CARIDEN_HOME>/lib:<CARIDEN_HOME>/lib/python`, re-exec with `os.execv` if
`IcePy` is not yet importable. A wrapper script is allowed only as comments
in the module docstring; the Python file itself must start unaided.
**Open plan.** `open_plan(path, host=..., port=..., protocol="ssl", linger=False)`.
Never `https`. IceSSL files are not constructor arguments.
**Baseline reset.** Always:
```python
model.route_simulation = [] # empty failure list; never None
model.traffic_simulation = None
model.route_simulation.recompute()
```
`route_simulation = None` leaves `_failed_objects` as a list, so
`circuit.failed = True` raises `AttributeError: 'list' object has no attribute
'add'`. Do not use `.failed = True` for what-if.
**Apply circuit failures.**
```python
model.route_simulation = [circuit, ...] # OPM circuit objects
model.route_simulation.recompute()
_ = model.traffic_simulation
```
**IGP.** `shortest_path` requires OPM Node objects:
```python
route = model.route_simulation.shortest_path(
model.nodes[src_name], model.nodes[dst_name], "igp"
)
```
Passing a string yields Ice `ValueError` on `getShortestPathRouteRecord` and
can poison the pooled communicator. Map SDK exceptions to ToolError with the
real exception class name; never claim a type error is a missing certificate.
**Order hops.** `Route.interfaces` is unordered. Reconstruct order by walking
from the source node: choose the interface whose node name equals the current
node, then advance via `opposite_interface.node` (or the other circuit
endpoint). Use that walk for IGP fill between node SIDs and from the last
anchor to the destination. Emit `resolved_ordered_path` as `"{node}:{iface}"`
strings. This topology walk is required; skipping IGP fill because the SDK
collection is unordered is incorrect.
**Circuit IDs.** Accept unique `node_a`+`node_b` (reject parallels), SDK
`ckt{n1|if1|n2|if2}`, and shorthand `n1-n2` / `n2-n1`.
**Simulation Analysis.**
```python
sa = SimulationAnalysis(model, failure_types=names, max_fail_per_int=n)
sa.cache_valid = True
for iface in model.interfaces:
rec = sa.interfaces[iface] # index by model interface
util = rec.worst_case_utilization
```
Stable `interface_id` (`if{node|name}` or `{node}:{name}`). Never a wrapper
`repr`. Do not iterate SA wrappers as if they were model interfaces.
**Growth.** kebab-case GenericTool JSON and DesignAPI filesystem round-trip:
1. Set `demand.growth_percent` on active demands.
2. `network.rpc_plan.serializeToFileSystem("/tmp/mcp_growth_<unique>.txt")`.
3. `pm.newPlanFromFileSystem(server_path)`; `newGenericTool().runTool(net,
"create_growth_plans", json.dumps({
"plan-file": server_path,
"create-plans-from": "DEMANDS",
"num-periods": num_periods,
"period-inc": period_inc,
"growth-method": "COMPOUND",
"run-sim-analysis": False,
}))`.
4. If the tool **raises** or reports success without changing demand traffic,
apply compound `(1 + g/100) ** (period_inc * num_periods)` via
`DemandTrafficKey` / `Demand.traffic` and set `fallback_used` /
`compound_fallback_applied`. Do not fail the tool solely because
`create_growth_plans` threw when fallback can still produce COMPOUND results.
5. `finally`: remove local temps and extra PlanManager keys.
6. SIMPLE: linear `1 + (g/100)*periods` through a verified API, or reject
SIMPLE explicitly. Never label compound math as SIMPLE.
## MCP tools
Implement every tool below with detailed docstrings, validated bounded inputs,
deterministically ordered outputs, and the common response/error conventions.
### Plan lifecycle and catalogs
1. `upload_plan(name, content, encoding="text", plan_id=None, validate=True, confirm=False)`
- stage and register content;
- optionally open it with OPM before publishing it as ready;
- remove all state if validation fails;
- return plan ID, canonical reference, safe metadata, digest, validation
state, and model counts;
- if `validate=False`, set state to `unchecked`, return null model counts,
and require successful validation before any analysis operation.
2. `list_uploaded_plans()`.
3. `delete_uploaded_plan(plan_id, confirm=False)`.
4. `designapi_health_check(plan_ref=None)`.
5. `get_plan_summary(plan_ref=None)`.
6. `list_circuits(plan_ref=None, node_filter=None, active_only=True, limit=100)`
- include stable circuit identity, endpoints, interfaces, capacity, and
active state;
- cap the limit and report truncation based on whether additional matches
actually exist.
7. `list_demands(plan_ref=None, source=None, destination=None, active_only=True, limit=100)`
- include stable identity/name, endpoints, service class, traffic with
units, and active state;
- cap the limit and report true truncation.
### Route and LSP inspection
8. `get_demand_path(source, destination, plan_ref=None, demand_name=None, service_)`
- reset failures and traffic state;
- recompute baseline routing;
- reject ambiguous demand matches;
- return routed state, traffic and units, metric, latency, ECMP percentage,
interfaces, traffic shares, and hop count.
9. `get_igp_path(source, destination, plan_ref=None, metric="igp")`
- allow only metrics verified for the installed SDK, such as IGP, BGP, TE,
and latency;
- resolve endpoints with `model.nodes[name]` and call `shortest_path` with
those Node objects;
- return the SDK-provided path while explicitly stating whether ordering is
guaranteed; if unordered, also return a topology-walk ordered list.
10. `list_lsps(plan_ref=None, source=None, destination=None, active_only=True, limit=100)`
- include stable identity, type, setup bandwidth and units, color, state,
and path options.
11. `get_lsp_path(source, plan_ref=None, lsp_name=None, destination=None, color=None, destination_ip=None, path_option=None)`
- resolve a named RSVP-TE LSP or an SR policy identified by source,
destination, and color;
- reject incomplete or ambiguous identifiers;
- support a specific path option;
- reset simulation state and recompute baseline routing;
- return routed state, type, endpoints, setup bandwidth, color, metric,
latency, and ECMP data;
- return configured segment-list steps;
- expand interface SIDs directly;
- expand node SIDs with SDK-provided IGP shortest-path fill using Node
objects plus the topology walk in the operational contract;
- append IGP fill from the final segment anchor to the destination using
the same walk;
- report unsupported or unresolved segment steps as warnings;
- clearly distinguish `resolved_ordered_path` (`"{node}:{iface}"` strings),
unordered simulation `interface_set`, and unconstrained
`reference_igp_shortest_path` (also Node-object `shortest_path`);
- never fabricate order by iterating a set. The topology walk above is the
required reconstruction; do not omit IGP fill because the SDK collection
is unordered.
### Explicit deterministic failure simulation
12. `failure_sim(plan_ref=None, node_a=None, node_b=None, failed_circuits=None, top_n=25, min_traffic_delta_mbps=1.0, sample_reroutes=5)`
- accept exactly one selector form: a non-empty circuit-ID list or a
complete node pair;
- resolve every circuit uniquely before changing simulation state; reject
a node pair matching parallel circuits and require explicit circuit IDs;
- treat `ckt{...}`, `nodeA-nodeB`, and `nodeB-nodeA` as circuit-ID list
forms;
- compute baseline demand paths and per-interface traffic/utilization;
- apply exactly the requested circuit failures with
`model.route_simulation = [circuits]` (not `.failed = True`) and recompute;
- return applied circuit IDs, active-demand count, rerouted and unrouted
counts, bounded sample reroutes with before/after paths, largest bounded
interface traffic changes, before/after utilization, newly
oversubscribed interfaces, and baseline oversubscription count;
- use deterministic tie-breaking and state units;
- do not mutate the stored baseline.
This tool is only for a known deterministic what-if; it is not a worst-case
engine.
### Worst-case Simulation Analysis
13. `get_wc_traffic(plan_ref=None, failure_sets="nodes,circuits", max_fail_per_int=10, limit=15)`
- normalize string or list input;
- validate failure-set names against an explicit allowlist proven valid for
the installed release;
- invoke Cisco `SimulationAnalysis` using its verified API;
- read worst-case utilization via `sa.interfaces[model_iface]` for each
model interface; never use a wrapper object's `repr` as an id;
- rank interfaces by vendor-produced worst-case utilization with stable
tie-breaking;
- return failure sets, units, bounded top interfaces, relevant vendor
warnings, and scenario count only if the verified API exposes it; never
infer a count by guessed enumeration.
Never implement this by repeatedly calling `failure_sim`, enumerating guessed
failures, or relabeling ordinary utilization as worst-case output. If the
installed SDK does not expose the required API, apply the prerequisite gate and
do not generate the server. If the verified API exists but is unlicensed or
unavailable in the deployed service, return a runtime capability error.
### Traffic growth
14. `get_traffic_growth(plan_ref=None, growth_percent=33.0, num_periods=1, period_inc=1, growth_method="COMPOUND", threshold=100.0, near_capacity=80.0)`
- validate finite numeric values and sensible bounded ranges;
- require `0 <= near_capacity <= threshold`;
- collect baseline per-circuit utilization, checking both sides and
reporting the bottleneck side;
- set growth attributes required by the installed SDK;
- use the verified Design plan-manager transfer lifecycle to create a
uniquely named server-side working plan from the uploaded artifact,
retain its returned key rather than guessing a server path, and retrieve
the generated result; if the release supports only a documented
server-visible filesystem path, validate that deployment prerequisite
explicitly;
- invoke `create_growth_plans` through Design RPC using the kebab-case
options and `serializeToFileSystem` / `newPlanFromFileSystem` lifecycle
in the operational contract (not camelCase, not `newPlanFromBytes` alone);
- inspect the generated plan and verify whether expected demand growth was
actually applied;
- if the tool raises, returns no new plan key, or reports success without
applying COMPOUND growth, a compatibility adapter must apply the
mathematically correct compound multiplier through the documented
demand-traffic RPC API and report that fallback;
- for SIMPLE growth, implement the documented mathematics through a
verified API or reject it explicitly; never use compound arithmetic while
labeling it SIMPLE;
- create local temporary files with mode `0600` inside the private staging
filesystem, serialize and reopen through OPM, and clean both local and
server-side temporary plans by their verified keys in `finally` blocks on
success or exception; after a soft timeout, cleanup must occur when the
underlying call actually returns rather than pretending it was cancelled;
- return multiplier, method, periods, whether tool growth or a compatibility
fallback was applied, baseline utilization summary, after-growth
oversubscriptions, and near-capacity circuits.
## Simulation correctness
- Reset failure, route, and traffic state before every baseline with
`model.route_simulation = []` (never `None`).
- Recompute after applying failures via `model.route_simulation = [circuits]`.
- Include only active objects where requested.
- Handle missing traffic, utilization, capacity, latency, and route values
safely without converting “unknown” to zero unless that meaning is documented.
- Compare both circuit sides and report the actual bottleneck side.
- Sort every ranked or sampled result deterministically with a stable secondary
key.
- Never claim an unordered collection is a hop sequence.
- Keep all mutable simulation state local to one opened plan context.
- Preserve and verify units in field names or adjacent metadata.
- Apply explicit hard caps to circuit IDs, catalog rows, path interfaces,
segment steps, reroutes, utilization rows, warnings, audit-field lengths, and
total serialized response size; include truncation metadata.
## Certificate and cryptographic requirements
Never disable TLS verification or use custom cryptography, MD5, SHA-1, DES,
3DES, RC4, Blowfish, AES-CBC, or AES-ECB. Use SHA-256 only for non-secret
content digests; token authentication must use a documented verifier and
constant-time comparison where applicable.
Do not embed or return certificate or private-key content. When expected X.509
certificate files are configured or discovered, flag them for operator
verification and document this command:
```bash
env -u LD_LIBRARY_PATH openssl x509 -text -noout -in <certificate_file>
```
If `LD_LIBRARY_PATH` includes the SDK `lib` directory, the system openssl CLI
fails (`undefined symbol: EVP_sm3`) unless you unset that variable.
The operator must verify:
- `notBefore` and `notAfter` validity;
- RSA keys are at least 2048 bits or EC keys use P-256 or stronger;
- signatures use SHA-2 or stronger, never MD5 or SHA-1;
- whether issuer and subject indicate a self-signed certificate and, if so,
that its use is intentional and limited to private development/testing or an
explicitly managed internal trust domain.
When configured certificate files are present, parse the public certificates
with a maintained library or documented standard-library facility and fail
closed on expiry/not-yet-valid status, RSA keys below 2048 bits, EC curves below
P-256, or MD5/SHA-1 signatures. Report self-signed status as an operator warning
and never expose certificate contents. `MCP_TOKEN_FILE` and other application
secret files must be mode `0600`/`0400` (fail closed). IceSSL private keys under
`CARIDEN_HOME/etc/certs`: if the file exists and is owner-readable, log a
warning to stderr when the mode is not `0600`/`0400`, but do **not** refuse
startup solely because a lab key is group-readable (`0664`). Still fail closed
if the IceSSL key is missing, unreadable by the process, or world-writable.
Do not read or log key content outside the TLS/SDK facility that consumes it.
## Required validation before completion
If the prerequisite gate failed, stop here and return only the `status:
blocked` evidence report; the file-specific checks below do not apply.
If the prerequisite gate passed, before presenting the generated file classify
checks as (a) mandatory offline,
(b) SDK-present offline, (c) live DesignAPI/licensing, or (d) deployment
operator checks. All applicable offline checks must pass. Run live/deployment
checks only when suitable services, identities, fixtures, and certificate files
are actually available; report the others as `not run`, never as passed.
Then:
1. Compile the file with the target Python interpreter.
2. Run Ruff or an equivalent static check if available.
3. Exercise a no-network `--check` path.
4. Enumerate registrations and confirm all 5 exact resource URI templates, 2
prompts, and 14 tools above are registered.
5. Confirm all referenced functions, imports, and compatibility adapters exist.
6. Confirm stdio mode writes no banner or logs to stdout.
7. Search the file for embedded secrets, private keys, certificates, tokens,
deployment addresses, credentialed URLs, weak cryptography, and disabled TLS
verification.
8. Test traversal, symlink, oversized content, malformed base64, ID collision,
failed validation cleanup, unauthorized cross-owner lookup, and deletion
confirmation.
9. Test concurrent registry operations and analysis-versus-delete behavior.
10. Confirm direct plan paths cannot escape allowed roots and cannot be used by
HTTP principals.
11. Confirm worst-case analysis calls the verified Cisco
`SimulationAnalysis`, never `failure_sim`.
12. Confirm SIMPLE growth is correct or explicitly rejected.
13. Confirm temporary files and Design-side temporary plans are cleaned up when
each intermediate operation fails.
14. Confirm HTTP cannot start without valid authentication and cannot bind
non-loopback without explicit authorization.
15. Confirm one authenticated principal cannot infer, read, operate on, or
delete another principal's plans.
16. Confirm `os.execv` (or equivalent) is used so `python3 cp_sample_mcp.py`
works without a pre-exported `LD_LIBRARY_PATH`.
17. If `CW_DESIGNAPI_HOST` is set and `us_wan.txt` is available, run live
DesignAPI checks and fix failures: upload (33/50/95); demand path
er1.sjc→er1.mia hop_count 6 metric ~2169; IGP cr1.sjc→cr1.kcy with Node
objects and no Ice ValueError; `failure_sim` node pair rerouted_count 10;
`failure_sim` `failed_circuits=["cr1.sjc-cr1.kcy"]`; `get_wc_traffic`
circuits with ~113% and human-readable interface ids; `get_traffic_growth`
33% one period with `ok` true and either tool growth or fallback.
Fix all failures before answering. Do not claim checks that could not run.
End with a concise report containing:
- the file created;
- compile/static-check results;
- detected Python, FastMCP, and Cisco SDK versions;
- the evidence source used for each version-sensitive Cisco capability;
- tests actually run;
- any remaining deployment-only checks, especially live DesignAPI connectivity,
licensing, and operator certificate verification.