Skip to content

Commit e53c9ce

Browse files
committed
mcp: First version of server
Signed-off-by: Simon Bennetts <psiinon@gmail.com>
1 parent 31a1abc commit e53c9ce

File tree

69 files changed

+6433
-72
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+6433
-72
lines changed

addOns/automation/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this add-on will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## Unreleased
7+
### Added
8+
- Access to the progress of long running jobs
9+
710
### Changed
811
- Move the Automation panel to the workspace window.
912
- Use the main output panel for plan output messages.

addOns/automation/src/main/java/org/zaproxy/addon/automation/ExtensionAutomation.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.Map.Entry;
3939
import java.util.SortedSet;
4040
import java.util.TreeSet;
41+
import java.util.concurrent.ConcurrentHashMap;
4142
import java.util.function.Predicate;
4243
import java.util.stream.Collectors;
4344
import javax.swing.Timer;
@@ -117,6 +118,7 @@ public class ExtensionAutomation extends ExtensionAdaptor implements CommandLine
117118
private AutomationParam param;
118119
private LinkedHashMap<Integer, AutomationPlan> plans = new LinkedHashMap<>();
119120
private List<AutomationPlan> runningPlans = Collections.synchronizedList(new ArrayList<>());
121+
private final Map<String, LongRunningJob> longRunningJobs = new ConcurrentHashMap<>();
120122

121123
private CommandLineArgument[] arguments = new CommandLineArgument[5];
122124
private static final int ARG_AUTO_RUN_IDX = 0;
@@ -185,6 +187,8 @@ public void hook(ExtensionHook extensionHook) {
185187

186188
@Override
187189
public void sessionChanged(Session session) {
190+
longRunningJobs.clear();
191+
188192
// Work around for core bug - can be removed once the core is fixed and
189193
// released
190194
String authHeaderValueVar = System.getenv(ZAP_AUTH_HEADER_VALUE);
@@ -398,6 +402,78 @@ public List<AutomationPlan> getRunningPlans() {
398402
return Collections.unmodifiableList(runningPlans);
399403
}
400404

405+
protected void registerLongRunningJob(LongRunningJob job) {
406+
// Need to register the job before it is run, so expect the jobId to be null to start with
407+
new Thread() {
408+
@Override
409+
public void run() {
410+
while (job.getId() == null) {
411+
try {
412+
sleep(200);
413+
} catch (InterruptedException e) {
414+
// Ignore
415+
}
416+
}
417+
longRunningJobs.put(job.getId(), job);
418+
}
419+
}.start();
420+
}
421+
422+
/**
423+
* Returns all known IDs of long-running jobs that have been started. Jobs remain tracked after
424+
* completion so their status can be queried. The map is cleared when a new ZAP session is
425+
* started.
426+
*
427+
* @return a list of job IDs
428+
* @since 0.59.0
429+
*/
430+
public List<String> getLongRunningJobIds() {
431+
return new ArrayList<>(longRunningJobs.keySet());
432+
}
433+
434+
/**
435+
* Returns the progress for the specified job ID.
436+
*
437+
* @param id the job id
438+
* @return the progress percentage (0-100), or -1 if the job is not found
439+
* @since 0.59.0
440+
*/
441+
public int getProgress(String id) {
442+
LongRunningJob job = longRunningJobs.get(id);
443+
return job != null ? job.getProgress() : -1;
444+
}
445+
446+
/**
447+
* Returns the progress of all known long-running jobs.
448+
*
449+
* @return a map of job ID to progress percentage (0-100)
450+
* @since 0.59.0
451+
*/
452+
public Map<String, Integer> getAllProgress() {
453+
Map<String, Integer> result = new HashMap<>();
454+
for (Entry<String, LongRunningJob> entry : longRunningJobs.entrySet()) {
455+
result.put(entry.getKey(), entry.getValue().getProgress());
456+
}
457+
return result;
458+
}
459+
460+
/**
461+
* Stops a long-running job by ID. The job must implement {@link AutomationJob} for stopping to
462+
* take effect.
463+
*
464+
* @param id the job id
465+
* @return {@code true} if the job was found and stopped, {@code false} otherwise
466+
* @since 0.59.0
467+
*/
468+
public boolean stopLongRunningJob(String id) {
469+
LongRunningJob job = longRunningJobs.get(id);
470+
if (job instanceof AutomationJob automationJob) {
471+
automationJob.stop();
472+
return true;
473+
}
474+
return false;
475+
}
476+
401477
public void stopPlan(AutomationPlan plan) {
402478
plan.stopPlan();
403479
}
@@ -500,6 +576,9 @@ public AutomationProgress runPlan(AutomationPlan plan, boolean resetProgress) {
500576
timer.start();
501577
}
502578
try {
579+
if (job instanceof LongRunningJob lrJob) {
580+
registerLongRunningJob(lrJob);
581+
}
503582
job.runJob(env, progress);
504583
} catch (Exception e) {
505584
LOGGER.error(e.getMessage(), e);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Zed Attack Proxy (ZAP) and its related class files.
3+
*
4+
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
5+
*
6+
* Copyright 2026 The ZAP Development Team
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
package org.zaproxy.addon.automation;
21+
22+
/**
23+
* Marker interface for automation jobs that run for an extended period.
24+
*
25+
* <p>Jobs implementing this interface provide a unique identifier and progress information.
26+
*/
27+
public interface LongRunningJob {
28+
29+
/**
30+
* Returns a unique identifier for this job instance.
31+
*
32+
* @return the job id, or {@code null} if the job has not yet started and obtained an id
33+
*/
34+
String getId();
35+
36+
/**
37+
* Returns the progress of the job as a percentage (0-100).
38+
*
39+
* @return the progress percentage
40+
*/
41+
int getProgress();
42+
}

addOns/automation/src/main/java/org/zaproxy/addon/automation/jobs/ActiveScanJob.java

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.zaproxy.addon.automation.AutomationProgress;
4242
import org.zaproxy.addon.automation.ContextWrapper;
4343
import org.zaproxy.addon.automation.JobResultData;
44+
import org.zaproxy.addon.automation.LongRunningJob;
4445
import org.zaproxy.addon.automation.gui.ActiveScanJobDialog;
4546
import org.zaproxy.addon.commonlib.Constants;
4647
import org.zaproxy.zap.extension.ascan.ActiveScan;
@@ -49,7 +50,7 @@
4950
import org.zaproxy.zap.model.Target;
5051
import org.zaproxy.zap.users.User;
5152

52-
public class ActiveScanJob extends AutomationJob {
53+
public class ActiveScanJob extends AutomationJob implements LongRunningJob {
5354

5455
public static final String JOB_NAME = "activeScan";
5556
private static final String OPTIONS_METHOD_NAME = "getScannerParam";
@@ -65,6 +66,8 @@ public class ActiveScanJob extends AutomationJob {
6566
private PolicyDefinition policyDefinition = new PolicyDefinition();
6667
private Data data;
6768
private boolean forceStop;
69+
private volatile Integer scanId;
70+
private volatile ActiveScan currentScan;
6871

6972
public ActiveScanJob() {
7073
data = new Data(this, this.parameters, this.policyDefinition);
@@ -248,38 +251,43 @@ public void runJob(AutomationEnvironment env, AutomationProgress progress) {
248251
}
249252

250253
forceStop = false;
251-
int scanId = this.getExtAScan().startScan(target, user, contextSpecificObjects.toArray());
252-
253-
long endTime = Long.MAX_VALUE;
254-
if (JobUtils.unBox(this.getParameters().getMaxScanDurationInMins()) > 0) {
255-
// The active scan should stop, if it doesnt we will stop it (after a few seconds
256-
// leeway)
257-
endTime =
258-
System.currentTimeMillis()
259-
+ TimeUnit.MINUTES.toMillis(
260-
this.getParameters().getMaxScanDurationInMins())
261-
+ TimeUnit.SECONDS.toMillis(5);
262-
}
263-
264-
// Wait for the active scan to finish
265-
ActiveScan scan;
254+
scanId = this.getExtAScan().startScan(target, user, contextSpecificObjects.toArray());
255+
ActiveScan scan = this.getExtAScan().getScan(scanId);
256+
try {
257+
currentScan = scan;
258+
259+
long endTime = Long.MAX_VALUE;
260+
if (JobUtils.unBox(this.getParameters().getMaxScanDurationInMins()) > 0) {
261+
// The active scan should stop, if it doesnt we will stop it (after a few seconds
262+
// leeway)
263+
endTime =
264+
System.currentTimeMillis()
265+
+ TimeUnit.MINUTES.toMillis(
266+
this.getParameters().getMaxScanDurationInMins())
267+
+ TimeUnit.SECONDS.toMillis(5);
268+
}
266269

267-
while (true) {
268-
this.sleep(500);
269-
scan = this.getExtAScan().getScan(scanId);
270-
if (scan.isStopped() || forceStop) {
271-
break;
270+
// Wait for the active scan to finish
271+
while (true) {
272+
this.sleep(500);
273+
scan = this.getExtAScan().getScan(scanId);
274+
currentScan = scan;
275+
if (scan.isStopped() || forceStop) {
276+
break;
277+
}
278+
if (!this.runMonitorTests(progress) || System.currentTimeMillis() > endTime) {
279+
forceStop = true;
280+
break;
281+
}
272282
}
273-
if (!this.runMonitorTests(progress) || System.currentTimeMillis() > endTime) {
274-
forceStop = true;
275-
break;
283+
if (forceStop) {
284+
this.getExtAScan().stopScan(scanId);
285+
progress.info(Constant.messages.getString("automation.info.jobstopped", getType()));
276286
}
287+
progress.addJobResultData(createJobResultData(scanId));
288+
} finally {
289+
currentScan = null;
277290
}
278-
if (forceStop) {
279-
this.getExtAScan().stopScan(scanId);
280-
progress.info(Constant.messages.getString("automation.info.jobstopped", getType()));
281-
}
282-
progress.addJobResultData(createJobResultData(scanId));
283291

284292
getExtAScan().setPanelSwitch(true);
285293
}
@@ -289,6 +297,20 @@ public void stop() {
289297
forceStop = true;
290298
}
291299

300+
@Override
301+
public String getId() {
302+
return scanId != null ? "ascan-" + scanId : null;
303+
}
304+
305+
@Override
306+
public int getProgress() {
307+
ActiveScan scan = currentScan;
308+
if (scan != null) {
309+
return scan.getProgress();
310+
}
311+
return getStatus() == Status.COMPLETED ? 100 : 0;
312+
}
313+
292314
@Override
293315
public List<JobResultData> getJobResultData() {
294316
ActiveScan lastScan = this.getExtAScan().getLastScan();

addOns/automation/src/test/java/org/zaproxy/addon/automation/jobs/ActiveScanJobUnitTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import org.zaproxy.addon.automation.AutomationJob.Order;
7676
import org.zaproxy.addon.automation.AutomationProgress;
7777
import org.zaproxy.addon.automation.ContextWrapper;
78+
import org.zaproxy.addon.automation.ExtensionAutomation;
7879
import org.zaproxy.zap.extension.ascan.ActiveScan;
7980
import org.zaproxy.zap.extension.ascan.ExtensionActiveScan;
8081
import org.zaproxy.zap.extension.ascan.PolicyManager;
@@ -128,6 +129,9 @@ void setUp() throws Exception {
128129
policyManager = mock();
129130
given(extAScan.getPolicyManager()).willReturn(policyManager);
130131

132+
ExtensionAutomation extAutomation = mock(ExtensionAutomation.class);
133+
given(extensionLoader.getExtension(ExtensionAutomation.class)).willReturn(extAutomation);
134+
131135
Control.initSingletonForTesting(Model.getSingleton(), extensionLoader);
132136
Model.getSingleton().getOptionsParam().load(new ZapXmlConfiguration());
133137
}
@@ -145,6 +149,8 @@ void shouldReturnDefaultFields() {
145149
assertThat(job.getOrder(), is(equalTo(Order.ATTACK)));
146150
assertThat(job.getParamMethodObject(), is(extAScan));
147151
assertThat(job.getParamMethodName(), is("getScannerParam"));
152+
assertThat(job.getId(), is(equalTo(null)));
153+
assertThat(job.getProgress(), is(equalTo(0)));
148154
}
149155

150156
@Test

addOns/mcp/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changelog
2+
All notable changes to this add-on will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5+
6+
## Unreleased
7+
8+
- First version with initial resources and tools.

addOns/mcp/gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
version=0.0.1

addOns/mcp/mcp.gradle.kts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
description = "An add-on that implements an MCP server in ZAP."
2+
3+
zapAddOn {
4+
addOnName.set("MCP Server")
5+
6+
manifest {
7+
author.set("ZAP Dev Team")
8+
extensions {
9+
register("org.zaproxy.addon.mcp.ExtensionMcp")
10+
}
11+
dependencies {
12+
addOns {
13+
register("automation") {
14+
version.set(">=0.31.0")
15+
}
16+
register("commonlib") {
17+
version.set(">=1.17.0")
18+
}
19+
register("network") {
20+
version.set(">=0.1.0")
21+
}
22+
}
23+
}
24+
}
25+
}
26+
27+
dependencies {
28+
zapAddOn("automation")
29+
zapAddOn("commonlib")
30+
zapAddOn("network")
31+
32+
testImplementation(project(":testutils"))
33+
}
34+
35+
crowdin {
36+
configuration {
37+
val resourcesPath = "org/zaproxy/addon/${zapAddOn.addOnId.get()}/resources/"
38+
tokens.put("%messagesPath%", resourcesPath)
39+
tokens.put("%helpPath%", resourcesPath)
40+
}
41+
}

0 commit comments

Comments
 (0)