Skip to content

Commit 2bcac2e

Browse files
committed
feat: expose live commands to MCP
1 parent 1ce46e0 commit 2bcac2e

12 files changed

Lines changed: 162 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66

77
## [Unreleased]
88

9+
## [0.16.0] — 2026-09-30
10+
11+
### Added ✨
12+
13+
- `send_live_command` delivers a transient command to an owned live run without restarting it. It explains handler requirements, non-persistence, accepted-versus-completed semantics, and safe retry limits.
14+
- `list_strategies` and `list_datasets` can include deleted entries for catalogue reconciliation; `get_account` reports the Cartesian sweep limit.
15+
16+
### Changed 🔄
17+
18+
- Build against `com.github.QTSurfer:sdk-java` `0.27.0`, which supplies the refreshed API client and live-command helper.
19+
920
## [0.15.0] — 2026-09-23
1021

1122
### Added ✨

‎README.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ MCP transport: stdio (stdin/stdout JSON-RPC 2.0)
163163
|------|-------------|
164164
| `upload_dataset` | Atomically create or version a dataset, stream a guarded local CSV or Parquet file and start ingest; never exposes a presigned URL |
165165
| `import_dataset` / `get_dataset_import` | Fetch DEX history into a dataset and poll its asynchronous fetch and ingest state |
166-
| `list_datasets` / `get_dataset` | List datasets or read one dataset's metadata and current version |
166+
| `list_datasets` / `get_dataset` | List datasets (optionally including deleted entries) or read one dataset's metadata and current version |
167167
| `get_dataset_upload` | Poll the asynchronous ingest after an upload |
168168
| `finalize_dataset_upload` | Retry finalization after a successful PUT whose original MCP response was lost |
169169
| `delete_dataset` | Delete a caller-owned dataset and its versions |
@@ -180,13 +180,14 @@ MCP transport: stdio (stdin/stdout JSON-RPC 2.0)
180180
| `get_sweep_run_equity_curve` | Bounded normalized equity curve for one retained sweep trial |
181181
| `cancel_sweep` | Stop a running sweep between parameter vectors, keeping the rows already scored |
182182
| `get_sweep_sensitivity` | Which parameter mattered: marginals per axis, or one named interaction surface |
183-
| `list_strategies` | List every strategy registered under this account, most recently compiled first |
183+
| `list_strategies` | List every strategy registered under this account; optionally include deleted entries |
184184
| `delete_strategy` | Release a registered strategy |
185185
| `get_strategy_code` | Fetch the exact source last registered for a strategy id |
186-
| `get_account` / `get_account_usage` | Read account tier limits and current dataset, strategy, signal and shared-storage usage |
186+
| `get_account` / `get_account_usage` | Read account tier limits (including the Cartesian sweep cap) and current dataset, strategy, signal and shared-storage usage |
187187
| `start_live` / `get_live` / `stop_live` | Start a strategy's live run, inspect sandbox/live state, and request it to stop |
188188
| `list_live` / `list_public_live` | Page through all your own runs or browse currently-running public runs separately |
189189
| `update_live` / `update_live_params` | Change run visibility/metadata or queue updates to declared strategy parameters |
190+
| `send_live_command` | Send a transient event to a running strategy without restarting it |
190191
| `get_live_signals` | Read retained signals oldest-first with optional time/instrument filters and cursor pagination |
191192

192193
### Account and storage
@@ -217,13 +218,23 @@ catalog containing only public runs currently running and never exposes their ow
217218
Both accept optional `cursor` and `limit` (default 20, API maximum 100); pass `nextCursor` back
218219
unchanged to fetch the next page.
219220

221+
`list_datasets` and `list_strategies` default to active entries; set `includeDeleted=true` to include
222+
soft-deleted entries marked with `deletedAt`. `get_account` reports `maxSweepCartesian`, the
223+
maximum allowed Cartesian combination count for a sweep.
224+
220225
`update_live` takes `runId` plus at least one of `visibility`, `name`, or `description`.
221226
`update_live_params` takes `runId` and a non-empty `params` object containing only declared strategy
222227
properties; changes take effect at an event boundary. `get_live_signals` accepts `runId`, optional
223228
`sinceMs`, `instrument`, opaque `cursor`, and `limit`; cursor takes precedence over `sinceMs`. Read
224229
oldest-first, pass returned `nextCursor` unchanged, and if retention expires a cursor, restart from
225230
the reported `availableSinceMs` (deduplicate `signalId` if combining with another signal feed).
226231

232+
`send_live_command` takes an owned `runId`, non-blank `command`, and optional arbitrary JSON
233+
`properties` object. The strategy must implement `CommandRequestHandler`. Commands are transient,
234+
not stored or replayed to later replicas; use `update_live_params` for persistent state. A `503`
235+
means the command was not sent; ambiguous network failures have no idempotency key, so avoid blind
236+
retries. A successful acceptance is not confirmation that the strategy finished processing it.
237+
227238
Example tool arguments:
228239

229240
```json

‎pom.xml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.qtsurfer</groupId>
77
<artifactId>mcp-java</artifactId>
8-
<version>0.15.0</version>
8+
<version>0.16.0</version>
99
<packaging>jar</packaging>
1010
<name>qtsurfer-mcp-java</name>
1111

@@ -34,7 +34,7 @@
3434
<maven.compiler.release>${java.version}</maven.compiler.release>
3535
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3636
<mcp.version>1.1.2</mcp.version>
37-
<sdk.version>0.25.1</sdk.version>
37+
<sdk.version>0.27.0</sdk.version>
3838
<slf4j.version>2.0.16</slf4j.version>
3939
<logback.version>1.5.11</logback.version>
4040
<junit.version>5.11.4</junit.version>

‎src/main/java/com/qtsurfer/mcp/model/DatasetSummary.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public record DatasetSummary(
99
String from,
1010
String to,
1111
String cadence,
12-
String dataFormat) {}
12+
String dataFormat,
13+
String deletedAt) {}

‎src/main/java/com/qtsurfer/mcp/service/BacktestingService.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import com.qtsurfer.api.client.model.AccountUsage;
1111
import com.qtsurfer.api.client.model.LiveListResponse;
1212
import com.qtsurfer.api.client.model.LiveParamsUpdateResult;
13+
import com.qtsurfer.api.client.model.LiveCommandResult;
1314
import com.qtsurfer.api.client.model.LiveRun;
1415
import com.qtsurfer.api.client.model.LiveRunCompact;
1516
import com.qtsurfer.api.client.model.LiveSignalPage;
1617
import com.qtsurfer.api.client.model.PublicLiveListResponse;
1718
import com.qtsurfer.api.client.model.StartLiveRequest;
1819
import com.qtsurfer.api.client.model.UpdateLiveRequest;
20+
import com.qtsurfer.api.client.model.SendLiveCommandRequest;
1921
import com.qtsurfer.api.client.model.SweepSensitivity;
2022
import com.qtsurfer.api.sdk.SweepObjective;
2123
import com.qtsurfer.api.sdk.ValidationOutcome;
@@ -57,7 +59,9 @@ public interface BacktestingService {
5759
StrategyCompilation compileStrategy(String strategyCode);
5860

5961
/** List datasets owned by the authenticated caller. */
60-
List<DatasetSummary> listDatasets();
62+
List<DatasetSummary> listDatasets(boolean includeDeleted);
63+
64+
default List<DatasetSummary> listDatasets() { return listDatasets(false); }
6165

6266
/** Read one dataset owned by the authenticated caller. */
6367
Optional<DatasetSummary> getDataset(String datasetId);
@@ -235,7 +239,9 @@ Optional<BoundedEquityCurve> getSweepRunEquityCurve(
235239
*
236240
* @return the caller's registered strategies; empty when the account has none, never an error
237241
*/
238-
List<StrategySummary> listStrategies();
242+
List<StrategySummary> listStrategies(boolean includeDeleted);
243+
244+
default List<StrategySummary> listStrategies() { return listStrategies(false); }
239245

240246
/**
241247
* Release a registered strategy. Not undone by recompiling the same source afterward — that
@@ -279,6 +285,8 @@ Optional<BoundedEquityCurve> getSweepRunEquityCurve(
279285

280286
LiveParamsUpdateResult updateLiveParams(String runId, Map<String, Object> params);
281287

288+
LiveCommandResult sendLiveCommand(String runId, SendLiveCommandRequest request);
289+
282290
LiveSignalPage getLiveSignals(String runId, Long sinceMs, String instrument,
283291
String cursor, Integer limit);
284292
}

‎src/main/java/com/qtsurfer/mcp/service/BacktestingServiceStub.java‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
import com.qtsurfer.api.client.model.AccountUsage;
1717
import com.qtsurfer.api.client.model.LiveListResponse;
1818
import com.qtsurfer.api.client.model.LiveParamsUpdateResult;
19+
import com.qtsurfer.api.client.model.LiveCommandResult;
1920
import com.qtsurfer.api.client.model.LiveRun;
2021
import com.qtsurfer.api.client.model.LiveRunCompact;
2122
import com.qtsurfer.api.client.model.LiveSignalPage;
2223
import com.qtsurfer.api.client.model.PublicLiveListResponse;
2324
import com.qtsurfer.api.client.model.StartLiveRequest;
2425
import com.qtsurfer.api.client.model.UpdateLiveRequest;
26+
import com.qtsurfer.api.client.model.SendLiveCommandRequest;
2527
import com.qtsurfer.api.client.model.SweepSensitivity;
2628
import com.qtsurfer.api.client.model.WalkForwardFold;
2729
import com.qtsurfer.api.client.model.WalkForwardResult;
@@ -83,7 +85,8 @@ public class BacktestingServiceStub implements BacktestingService {
8385
@Override
8486
public Account getAccount() {
8587
return new Account().userId("stub-user").tier("free")
86-
.maxDatasets(10).maxDatasetBytes(1_000_000L).maxTotalStorageBytes(10_000_000L);
88+
.maxDatasets(10).maxDatasetBytes(1_000_000L).maxTotalStorageBytes(10_000_000L)
89+
.maxSweepCartesian(10_000);
8790
}
8891

8992
@Override
@@ -150,6 +153,15 @@ public LiveParamsUpdateResult updateLiveParams(String runId, Map<String, Object>
150153
.effectiveAtMs(System.currentTimeMillis());
151154
}
152155

156+
@Override
157+
public LiveCommandResult sendLiveCommand(String runId, SendLiveCommandRequest request) {
158+
if (liveRuns.values().stream().noneMatch(candidate -> runId.equals(candidate.getRunId()))) {
159+
throw new IllegalArgumentException("No live run: " + runId);
160+
}
161+
return new LiveCommandResult().runId(runId).commandId("cmd-stub")
162+
.effectiveAtMs(System.currentTimeMillis());
163+
}
164+
153165
@Override
154166
public LiveSignalPage getLiveSignals(
155167
String runId, Long sinceMs, String instrument, String cursor, Integer limit) {
@@ -171,7 +183,7 @@ public StrategyCompilation compileStrategy(String strategyCode) {
171183
}
172184

173185
@Override
174-
public List<DatasetSummary> listDatasets() {
186+
public List<DatasetSummary> listDatasets(boolean includeDeleted) {
175187
return new ArrayList<>(datasets.values());
176188
}
177189

@@ -197,7 +209,7 @@ public DatasetUploadResult uploadDataset(
197209
}
198210
targetDatasetId = "ds-" + UUID.randomUUID().toString().substring(0, 8);
199211
datasets.put(targetDatasetId, new DatasetSummary(
200-
targetDatasetId, name, instrument, null, null, null, null, null));
212+
targetDatasetId, name, instrument, null, null, null, null, null, null));
201213
} else if (!datasets.containsKey(targetDatasetId)) {
202214
throw new IllegalArgumentException("No such dataset: " + targetDatasetId);
203215
}
@@ -226,7 +238,7 @@ public DatasetImportResult importDataset(String name, String instrument, String
226238
String datasetId = "ds-" + UUID.randomUUID().toString().substring(0, 8);
227239
String importId = "imp-" + UUID.randomUUID().toString().substring(0, 8);
228240
String jobId = "ing-" + UUID.randomUUID().toString().substring(0, 8);
229-
datasets.put(datasetId, new DatasetSummary(datasetId, name, instrument, null, from, to, null, null));
241+
datasets.put(datasetId, new DatasetSummary(datasetId, name, instrument, null, from, to, null, null, null));
230242
imports.put(uploadKey(datasetId, importId), new DatasetImportStatus(
231243
datasetId, importId, "FETCHING", jobId, null, null));
232244
return new DatasetImportResult(datasetId, importId, jobId, "FETCHING");
@@ -361,7 +373,7 @@ private StrategyState strategyState(String strategyId) {
361373
}
362374

363375
@Override
364-
public List<StrategySummary> listStrategies() {
376+
public List<StrategySummary> listStrategies(boolean includeDeleted) {
365377
return new ArrayList<>(strategies.values());
366378
}
367379

‎src/main/java/com/qtsurfer/mcp/service/SdkBacktestingService.java‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import com.qtsurfer.api.client.model.AccountUsage;
2525
import com.qtsurfer.api.client.model.LiveListResponse;
2626
import com.qtsurfer.api.client.model.LiveParamsUpdateResult;
27+
import com.qtsurfer.api.client.model.LiveCommandResult;
2728
import com.qtsurfer.api.client.model.LiveRun;
2829
import com.qtsurfer.api.client.model.LiveRunCompact;
2930
import com.qtsurfer.api.client.model.LiveSignalPage;
3031
import com.qtsurfer.api.client.model.PublicLiveListResponse;
3132
import com.qtsurfer.api.client.model.StartLiveRequest;
3233
import com.qtsurfer.api.client.model.UpdateLiveRequest;
34+
import com.qtsurfer.api.client.model.SendLiveCommandRequest;
3335
import com.qtsurfer.api.client.model.SweepSensitivity;
3436
import com.qtsurfer.api.sdk.Backtest;
3537
import com.qtsurfer.api.sdk.BacktestOptions;
@@ -154,6 +156,11 @@ public LiveParamsUpdateResult updateLiveParams(String runId, Map<String, Object>
154156
return qts.updateLiveParams(runId, UpdateLiveParamsRequestBuilder.builder().params(params));
155157
}
156158

159+
@Override
160+
public LiveCommandResult sendLiveCommand(String runId, SendLiveCommandRequest request) {
161+
return qts.sendLiveCommand(runId, request);
162+
}
163+
157164
@Override
158165
public LiveSignalPage getLiveSignals(
159166
String runId, Long sinceMs, String instrument, String cursor, Integer limit) {
@@ -221,8 +228,8 @@ public StrategyCompilation compileStrategy(String strategyCode) {
221228
}
222229

223230
@Override
224-
public List<DatasetSummary> listDatasets() {
225-
return qts.getDatasets().stream().map(SdkBacktestingService::datasetSummary).toList();
231+
public List<DatasetSummary> listDatasets(boolean includeDeleted) {
232+
return qts.getDatasets(includeDeleted).stream().map(SdkBacktestingService::datasetSummary).toList();
226233
}
227234

228235
@Override
@@ -322,13 +329,13 @@ private Path guardedUploadFile(String filePath) {
322329
private static DatasetSummary datasetSummary(Dataset dataset) {
323330
return new DatasetSummary(dataset.getDatasetId(), dataset.getName(), dataset.getInstrument(),
324331
dataset.getCurrentVersionId(), stringify(dataset.getFrom()), stringify(dataset.getTo()),
325-
dataset.getCadence(), null);
332+
dataset.getCadence(), null, stringify(dataset.getDeletedAt()));
326333
}
327334

328335
private static DatasetSummary datasetSummary(DatasetWithLinks dataset) {
329336
return new DatasetSummary(dataset.getDatasetId(), dataset.getName(), dataset.getInstrument(),
330337
dataset.getCurrentVersionId(), stringify(dataset.getFrom()), stringify(dataset.getTo()),
331-
dataset.getCadence(), stringify(dataset.getDataFormat()));
338+
dataset.getCadence(), stringify(dataset.getDataFormat()), stringify(dataset.getDeletedAt()));
332339
}
333340

334341
private static DatasetUploadStatus uploadStatus(String datasetId, DatasetUploadState state) {
@@ -640,8 +647,8 @@ public Optional<StrategyState> getStrategy(String strategyId) {
640647
}
641648

642649
@Override
643-
public List<StrategySummary> listStrategies() {
644-
return qts.getStrategies();
650+
public List<StrategySummary> listStrategies(boolean includeDeleted) {
651+
return qts.getStrategies(includeDeleted);
645652
}
646653

647654
@Override

0 commit comments

Comments
 (0)