Testing and Security Workflows
By the Syncfusion DevTutorials team · Last updated
Testing and security through MCP: test-runner and secret-scanner tools, unit and contract tests for MCP servers, end-to-end checks, and securing dev-tool servers with scopes, guards and least privilege.
Two of the most valuable things an assistant can do for developers are "run the tests and tell me what broke" and "check this for leaked secrets." Both need tools: the assistant cannot run a test suite or scan a repository by thinking about it. The servers that provide those tools are code too, so they need their own tests and their own security.
The main idea: MCP connects assistants to testing and security workflows through tools such as run_tests and scan_secrets that return structured, actionable results. Test MCP servers at three levels: unit tests for each handler, contract tests that catch accidental changes to tool names, schemas, and annotations, and end-to-end checks with the Inspector or a real host. Secure dev-tool servers with least-privilege scopes per tool, guards in code, masked secrets, and authorization checked on every call.
A Real-World Example
Omar's team connects its assistant to the dev workbench. On Monday:
- He asks, "Run the tests and tell me what failed." The assistant calls
run_tests, gets two structured failures, and explains them: the login button cannot be found by role (a frontend accessibility bug), andGET /ordersreturns 200 for another user's ID (a backend authorization bug). - He asks, "Did I leak anything in this branch?"
scan_secretsfinds a payment key inconfig/payments.tsand reports onlyacme...and the file. The assistant reminds him to rotate the key, not just delete it.
On Tuesday, a teammate "improves" the server by marking delete_branch as non-destructive. The server's contract test fails in CI: the annotation changed. The host would have started offering "Always allow" for a destructive tool. The change is reverted before release.
In simple words: let tools run the tests and scans, and test and lock down the tool servers like any other code.
What You Will Learn
In this lesson, you will learn:
- How a test-runner tool supports frontend and backend debugging.
- How a secret-scanner tool supports security work.
- How to unit-test MCP tool handlers.
- How contract tests protect tool schemas and annotations.
- How to secure dev-tool servers with scopes and guards.
Use the dev workbench server in the MCP Lab: run_tests and scan_secrets are real tools in it, and the Inspector shows every message. The Raw request tab's protected server (/secure/mcp) shows how scopes and 403 insufficient_scope work over HTTP (see Remote MCP and Authorization).
Testing Workflows
Run the Tests Through a Tool
A test-runner tool should return structured results: counts and a list of failures with names and messages. The model can then group failures and explain them.
Sort failures by area
// A test-runner tool returns structured results the model can act on.
const result = {
passed: 3,
failed: [
{
name: "LoginForm submits with the keyboard",
error: "Unable to find role 'button' with name 'Sign in'",
},
{
name: "GET /orders rejects another user's id",
error: "expected 403, received 200",
},
],
};
for (const f of result.failed) {
const area = /role|label|button/.test(f.error)
? "frontend/accessibility"
: /\b(40\d|50\d)\b/.test(f.error)
? "backend/authorization"
: "logic";
console.log(`${area.padEnd(24)} ${f.name}`);
}
console.log(`${result.passed} passed, ${result.failed.length} failed`);
Expected output:
frontend/accessibility LoginForm submits with the keyboard
backend/authorization GET /orders rejects another user's id
3 passed, 2 failed
Run the test suite
Run only the LoginForm tests
Keep test output short in the tool result. Return the failing tests and their first error line, not 2,000 lines of logs. If the model needs more, provide a second tool or a resource (ci://runs/{id}/log) it can read on demand.
Security Workflows
Scan for Secrets
A secret-scanner tool is an exact pattern-matching check: it is perfect for code, but it is not a substitute for a model. It reports the file location and a safe prefix only.
Scan files and mask the findings
// A secret scanner tool: find keys, report locations, never echo the full value.
const files = {
"config/payments.ts": "export const PAY_KEY = 'acme_live_4f9XyZ81QwErTy00';",
"src/App.tsx": "export default function App() { return null; }",
"scripts/deploy.sh": "export TOKEN=ghp_abcdefghijklmnopqrstuvwxyz0123",
};
const patterns = [
["payment key", /acme_live_[A-Za-z0-9]{8,}/g],
["GitHub-style token", /ghp_[A-Za-z0-9]{20,}/g],
];
for (const [path, text] of Object.entries(files)) {
for (const [label, re] of patterns) {
for (const m of text.matchAll(re)) {
console.log(`${path}: ${label} ${m[0].slice(0, 4)}...`);
}
}
}
Expected output:
config/payments.ts: payment key acme...
scripts/deploy.sh: GitHub-style token ghp_...
Scan the repository for secrets
Other security tools that fit MCP well are dependency audits, static analysis results, and policy checks. Each should be read-only, return structured findings, and never echo secrets.
Testing MCP Servers
Unit Tests for Handlers
Each tool handler is a function. Test it directly, without a model, including refusal cases.
Unit-test a tool handler
// Unit-test a tool handler directly, without any model in the loop.
function runQuery({ sql }) {
if (!/^\s*select\b/i.test(sql) || /;\s*\S/.test(sql)) {
return { isError: true, content: [{ type: "text", text: "Refused" }] };
}
return { content: [{ type: "text", text: "3 rows" }] };
}
const cases = [
[{ sql: "SELECT 1" }, false],
[{ sql: "DROP TABLE orders" }, true],
[{ sql: "select 1; delete from users" }, true],
];
for (const [input, expectError] of cases) {
const r = runQuery(input);
const ok = Boolean(r.isError) === expectError;
console.log(`${ok ? "PASS" : "FAIL"} ${JSON.stringify(input)} -> ${r.content[0].text}`);
}
Expected output:
PASS {"sql":"SELECT 1"} -> 3 rows
PASS {"sql":"DROP TABLE orders"} -> Refused
PASS {"sql":"select 1; delete from users"} -> Refused
Contract Tests for the Tool List
Hosts make decisions from your tool list: names, required arguments, and annotations. An accidental change can break hosts or weaken safety. Snapshot the list and compare it on every change.
Detect tool-list changes
// Contract test: the tool list should not change shape by accident.
const snapshot = {
run_query: { required: ["sql"], readOnlyHint: true },
delete_branch: { required: ["name"], readOnlyHint: false, destructiveHint: true },
};
const current = {
run_query: { required: ["sql", "database"], readOnlyHint: true },
delete_branch: { required: ["name"], readOnlyHint: false, destructiveHint: false },
};
for (const [tool, before] of Object.entries(snapshot)) {
const now = current[tool];
const changes = Object.keys(before).filter(
(k) => JSON.stringify(before[k]) !== JSON.stringify(now[k]),
);
console.log(
`${tool.padEnd(14)} ${changes.length ? "CHANGED: " + changes.join(", ") : "same"}`,
);
}
Expected output:
run_query CHANGED: required
delete_branch CHANGED: destructiveHint
| Change | Why it matters |
|---|---|
| A new required argument | Existing callers start failing |
destructiveHint true → false | Hosts may stop asking for confirmation |
readOnlyHint false → true | Hosts may run a write tool without asking |
| A renamed tool | Saved approvals and prompts stop matching |
End-to-End Checks
Finally, connect a real client: the MCP Inspector, a test host, or the lab. Check discovery, a successful call, a refused call, progress, cancellation, and authorization errors.
Watch the client and server talk
Securing Dev-Tool Servers
Scopes per Tool
Developer tools range from harmless (read tokens) to dangerous (delete branches). Give each tool a scope, and check the token's scopes on every call.
Check scopes per tool
// Least privilege for a dev-tools server: scopes per tool, checked on every call.
const toolScopes = {
get_design_tokens: "design:read",
run_query: "db:read",
run_tests: "ci:run",
create_branch: "repo:write",
delete_branch: "repo:admin",
};
const token = { scopes: ["design:read", "db:read", "ci:run", "repo:write"] };
for (const [tool, scope] of Object.entries(toolScopes)) {
const ok = token.scopes.includes(scope);
console.log(
`${tool.padEnd(18)} needs ${scope.padEnd(12)} ${ok ? "allowed" : "403 insufficient_scope"}`,
);
}
Expected output:
get_design_tokens needs design:read allowed
run_query needs db:read allowed
run_tests needs ci:run allowed
create_branch needs repo:write allowed
delete_branch needs repo:admin 403 insufficient_scope
See 403 insufficient_scope
Security Checklist for Dev-Tool Servers
- Read-only by default; write tools are separate, clearly annotated, and scoped.
- Guards in code (single SELECT, allowed paths, branch-name patterns), not only in descriptions.
- Least-privilege credentials for every backend system (read-only DB user, scoped repo token).
- Never return secrets; mask findings and redact logs.
- Validate inputs with schemas and reject paths such as
../../etc/passwd. - Treat tool inputs from the model as untrusted: they may come from prompt-injected content.
- Log every write call with the user, arguments, and result.
A test-runner tool executes code. If it accepts arbitrary commands or file paths, a prompt-injected request could run anything on the CI machine. Accept only a test filter, run in an isolated container with no production credentials, and time out long runs.
Common mistakes
- Dumping full logs into tool results. Return failures and first error lines.
- Echoing secrets in scan results. Show a prefix and the location.
- Testing servers only through a model. Unit-test handlers directly.
- No contract tests. Annotation and schema changes slip through.
- One scope for all tools. Scope each tool, and check on every call.
- Test runners that accept any command. Accept filters only, in isolation.
Check Your Understanding
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- Test-runner and secret-scanner tools bring testing and security workflows into the assistant.
- Return structured, short, actionable results, and mask secrets.
- Test servers with unit tests (handlers), contract tests (names, schemas, annotations), and end-to-end checks.
- Secure dev-tool servers with scopes per tool, guards in code, least-privilege credentials, and logging.
- Treat tool inputs as untrusted, and isolate anything that executes code.
This completes the Development Workflows chapter. Return to the MCP Reference page any time for message shapes, error codes, and checklists.