mcp-pacemaker 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +452 -0
- package/CONTRIBUTING.md +222 -0
- package/LICENSE +21 -0
- package/README.md +621 -0
- package/SECURITY.md +120 -0
- package/THIRD_PARTY_NOTICES.txt +366 -0
- package/autostart/windows/launcher.vbs +11 -0
- package/autostart/windows/register-task.ps1 +63 -0
- package/bin/cli.mjs +880 -0
- package/bin/config-checks.mjs +62 -0
- package/bin/logs.mjs +465 -0
- package/bin/mcp-bridge.mjs +2162 -0
- package/bin/pooling-batch-scheduler.mjs +129 -0
- package/bin/pooling-batches.mjs +270 -0
- package/bin/pooling-config.mjs +577 -0
- package/bin/pooling-editor.mjs +128 -0
- package/bin/pooling-errors.mjs +23 -0
- package/bin/pooling-execution.mjs +104 -0
- package/bin/pooling-files.mjs +541 -0
- package/bin/pooling-trace.mjs +218 -0
- package/bin/pooling-writer.mjs +242 -0
- package/bin/prewarm.mjs +233 -0
- package/bin/server-metrics.mjs +69 -0
- package/bin/service-control.mjs +124 -0
- package/bin/shared-sessions.mjs +1083 -0
- package/bin/top.mjs +115 -0
- package/bin/update-check.mjs +153 -0
- package/bin/windows/PoolingSecurityHelper.build.json +42 -0
- package/bin/windows/PoolingSecurityHelper.exe +0 -0
- package/bin/windows/src/AssemblyInfo.cs +11 -0
- package/bin/windows/src/PoolingNativeFiles.cs +678 -0
- package/bin/windows/src/PoolingSecurityHelper.cs +178 -0
- package/bin/windows/src/PoolingSecurityReader.cs +50 -0
- package/docs/shared-sessions.md +181 -0
- package/examples/servers.example.json +31 -0
- package/package.json +77 -0
- package/supervisor/bridge-child.mjs +14 -0
- package/supervisor/supervise.mjs +172 -0
- package/supervisor/supervise.ps1 +23 -0
- package/supervisor/supervise.sh +11 -0
- package/tools/windows-security-helper/README.md +238 -0
- package/tools/windows-security-helper/build.ps1 +262 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
- package/ui/dist/assets/index-CaLPg0c-.css +1 -0
- package/ui/dist/assets/index-CgPruWYi.js +9 -0
- package/ui/dist/index.html +14 -0
- package/ui/dist/third-party-manifest.json +597 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.ComponentModel;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Security.AccessControl;
|
|
5
|
+
using System.Security.Cryptography;
|
|
6
|
+
|
|
7
|
+
public sealed class PoolingSecurityHelper
|
|
8
|
+
{
|
|
9
|
+
private readonly PoolingSecurityReader nativeReader = new PoolingSecurityReader();
|
|
10
|
+
private readonly AccessControlSections sections =
|
|
11
|
+
AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group;
|
|
12
|
+
|
|
13
|
+
/// <summary>Inspects, stages, moves, or preserves Windows file security.</summary>
|
|
14
|
+
/// <param name="args">One action. File paths come from MCP_POOL environment variables.</param>
|
|
15
|
+
/// <returns>Zero on success, three on a source security conflict, or one on failure.</returns>
|
|
16
|
+
public static int Main(string[] args)
|
|
17
|
+
{
|
|
18
|
+
return new PoolingSecurityHelper().Run(args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// <summary>Runs a native file operation or a legacy security operation.</summary>
|
|
22
|
+
/// <param name="args">One of inspect-access, stage, move-no-replace, inspect, or copy.</param>
|
|
23
|
+
/// <returns>Zero on success, three on a source security conflict, or one on failure.</returns>
|
|
24
|
+
public int Run(string[] args)
|
|
25
|
+
{
|
|
26
|
+
try
|
|
27
|
+
{
|
|
28
|
+
if (args.Length != 1)
|
|
29
|
+
{
|
|
30
|
+
throw new ArgumentException();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var sourcePath = this.RequiredEnvironment("MCP_POOL_SOURCE");
|
|
34
|
+
if (args[0] == "inspect-access" ||
|
|
35
|
+
args[0] == "stage" ||
|
|
36
|
+
args[0] == "move-no-replace")
|
|
37
|
+
{
|
|
38
|
+
var files = new PoolingNativeFiles();
|
|
39
|
+
Console.WriteLine(files.Run(args[0], sourcePath));
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (args[0] == "inspect")
|
|
44
|
+
{
|
|
45
|
+
Console.WriteLine(this.ReadSecurity(sourcePath).Fingerprint);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (args[0] == "copy")
|
|
50
|
+
{
|
|
51
|
+
return this.CopySecurity(sourcePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new ArgumentException();
|
|
55
|
+
}
|
|
56
|
+
catch (Exception error)
|
|
57
|
+
{
|
|
58
|
+
Console.Error.WriteLine("MCPERR type=" + error.GetType().Name);
|
|
59
|
+
var nativeError = error as Win32Exception;
|
|
60
|
+
if (nativeError != null)
|
|
61
|
+
{
|
|
62
|
+
Console.Error.WriteLine("MCPERR nativeError=" + nativeError.NativeErrorCode);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
bool conflict = error is PoolingConflictException ||
|
|
66
|
+
(nativeError != null &&
|
|
67
|
+
(nativeError.NativeErrorCode == 80 ||
|
|
68
|
+
nativeError.NativeErrorCode == 183));
|
|
69
|
+
return conflict ? 3 : 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private string RequiredEnvironment(string name)
|
|
74
|
+
{
|
|
75
|
+
var value = Environment.GetEnvironmentVariable(name);
|
|
76
|
+
if (string.IsNullOrEmpty(value))
|
|
77
|
+
{
|
|
78
|
+
throw new ArgumentException();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private SecurityState ReadSecurity(string path)
|
|
85
|
+
{
|
|
86
|
+
var acl = File.GetAccessControl(path);
|
|
87
|
+
var descriptor = acl.GetSecurityDescriptorSddlForm(this.sections);
|
|
88
|
+
var native = this.nativeReader.ReadControlDescriptor(path);
|
|
89
|
+
bool complete = true;
|
|
90
|
+
var audit = new byte[0];
|
|
91
|
+
try
|
|
92
|
+
{
|
|
93
|
+
audit = this.ReadAudit(path);
|
|
94
|
+
}
|
|
95
|
+
catch (PrivilegeNotHeldException)
|
|
96
|
+
{
|
|
97
|
+
complete = false;
|
|
98
|
+
}
|
|
99
|
+
catch (UnauthorizedAccessException)
|
|
100
|
+
{
|
|
101
|
+
complete = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
int attributes = (int)File.GetAttributes(path);
|
|
105
|
+
using (var hash = SHA256.Create())
|
|
106
|
+
using (var buffer = new MemoryStream())
|
|
107
|
+
using (var writer = new BinaryWriter(buffer))
|
|
108
|
+
{
|
|
109
|
+
writer.Write(native.Length);
|
|
110
|
+
writer.Write(native);
|
|
111
|
+
writer.Write(audit.Length);
|
|
112
|
+
writer.Write(audit);
|
|
113
|
+
writer.Write(attributes);
|
|
114
|
+
writer.Flush();
|
|
115
|
+
var scope = complete ? "F:" : "P:";
|
|
116
|
+
return new SecurityState
|
|
117
|
+
{
|
|
118
|
+
Descriptor = descriptor,
|
|
119
|
+
Fingerprint = scope + Convert.ToBase64String(hash.ComputeHash(buffer.ToArray())),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private byte[] ReadAudit(string path)
|
|
125
|
+
{
|
|
126
|
+
return File.GetAccessControl(path, AccessControlSections.Audit).GetSecurityDescriptorBinaryForm();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private int CopySecurity(string sourcePath)
|
|
130
|
+
{
|
|
131
|
+
var expectedFingerprint = this.RequiredEnvironment("MCP_POOL_SECURITY");
|
|
132
|
+
var source = this.ReadSecurity(sourcePath);
|
|
133
|
+
if (!string.Equals(source.Fingerprint, expectedFingerprint, StringComparison.Ordinal))
|
|
134
|
+
{
|
|
135
|
+
return 3;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
var paths = new[]
|
|
139
|
+
{
|
|
140
|
+
this.RequiredEnvironment("MCP_POOL_TEMP"),
|
|
141
|
+
this.RequiredEnvironment("MCP_POOL_BACKUP"),
|
|
142
|
+
};
|
|
143
|
+
foreach (var path in paths)
|
|
144
|
+
{
|
|
145
|
+
var actual = File.GetAccessControl(path);
|
|
146
|
+
if (!string.Equals(actual.GetSecurityDescriptorSddlForm(this.sections),
|
|
147
|
+
source.Descriptor, StringComparison.Ordinal))
|
|
148
|
+
{
|
|
149
|
+
var copy = new FileSecurity();
|
|
150
|
+
copy.SetSecurityDescriptorSddlForm(source.Descriptor, this.sections);
|
|
151
|
+
File.SetAccessControl(path, copy);
|
|
152
|
+
actual = File.GetAccessControl(path);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!string.Equals(actual.GetSecurityDescriptorSddlForm(this.sections),
|
|
156
|
+
source.Descriptor, StringComparison.Ordinal))
|
|
157
|
+
{
|
|
158
|
+
throw new InvalidOperationException();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!string.Equals(this.ReadSecurity(path).Fingerprint,
|
|
162
|
+
expectedFingerprint, StringComparison.Ordinal))
|
|
163
|
+
{
|
|
164
|
+
throw new InvalidOperationException();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return string.Equals(this.ReadSecurity(sourcePath).Fingerprint,
|
|
169
|
+
expectedFingerprint, StringComparison.Ordinal) ? 0 : 3;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private sealed class SecurityState
|
|
173
|
+
{
|
|
174
|
+
public string Descriptor { get; set; }
|
|
175
|
+
|
|
176
|
+
public string Fingerprint { get; set; }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.ComponentModel;
|
|
3
|
+
using System.Runtime.InteropServices;
|
|
4
|
+
using System.Security.AccessControl;
|
|
5
|
+
|
|
6
|
+
public sealed class PoolingSecurityReader
|
|
7
|
+
{
|
|
8
|
+
private const uint ReadControlSections = 0x1f7;
|
|
9
|
+
|
|
10
|
+
/// <summary>Reads native owner, DACL and non-audit security sections.</summary>
|
|
11
|
+
/// <param name="path">File whose descriptor is queried.</param>
|
|
12
|
+
/// <returns>The unfiltered sections in canonical binary form.</returns>
|
|
13
|
+
public byte[] ReadControlDescriptor(string path)
|
|
14
|
+
{
|
|
15
|
+
var descriptor = IntPtr.Zero;
|
|
16
|
+
try
|
|
17
|
+
{
|
|
18
|
+
uint status = GetNamedSecurityInfo(path, 1, ReadControlSections,
|
|
19
|
+
IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out descriptor);
|
|
20
|
+
if (status != 0)
|
|
21
|
+
{
|
|
22
|
+
throw new Win32Exception((int)status);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var native = new byte[GetSecurityDescriptorLength(descriptor)];
|
|
26
|
+
Marshal.Copy(descriptor, native, 0, native.Length);
|
|
27
|
+
var raw = new RawSecurityDescriptor(native, 0);
|
|
28
|
+
var canonical = new byte[raw.BinaryLength];
|
|
29
|
+
raw.GetBinaryForm(canonical, 0);
|
|
30
|
+
return canonical;
|
|
31
|
+
}
|
|
32
|
+
finally
|
|
33
|
+
{
|
|
34
|
+
if (descriptor != IntPtr.Zero)
|
|
35
|
+
{
|
|
36
|
+
LocalFree(descriptor);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[DllImport("advapi32.dll", EntryPoint = "GetNamedSecurityInfoW", CharSet = CharSet.Unicode)]
|
|
42
|
+
private static extern uint GetNamedSecurityInfo(string path, uint objectType, uint sections,
|
|
43
|
+
IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl, out IntPtr descriptor);
|
|
44
|
+
|
|
45
|
+
[DllImport("advapi32.dll")]
|
|
46
|
+
private static extern uint GetSecurityDescriptorLength(IntPtr descriptor);
|
|
47
|
+
|
|
48
|
+
[DllImport("kernel32.dll")]
|
|
49
|
+
private static extern IntPtr LocalFree(IntPtr memory);
|
|
50
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Shared stdio sessions
|
|
2
|
+
|
|
3
|
+
Status: 1.3.0 implementation contract; not a claim that every server is compatible.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Add `"sharing": "shared"` to a stdio definition only after confirming the profile below.
|
|
8
|
+
Use `/<name>/mcp`: classic SSE endpoints explicitly reject shared mode.
|
|
9
|
+
Pooling buttons never enable shared mode or replace an existing shared policy.
|
|
10
|
+
|
|
11
|
+
| Setting | Default | Purpose |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `sharedLingerMs` | 1800000 | Keep the initialized child between compatible sessions. |
|
|
14
|
+
| `sharedDrainTimeoutMs` | 30000 | Maximum drain time before terminating unresolved work. |
|
|
15
|
+
| `sharedRetryDelayMs` | 1000 | Backoff after a failed generation. |
|
|
16
|
+
| `sharedMaxSessions` | 128 | Virtual members plus joining clients per group. |
|
|
17
|
+
| `sharedMaxInFlight` | 8 | Dispatched operations per child. |
|
|
18
|
+
| `sharedMaxQueued` | 32 | Unsent operations waiting for dispatch. |
|
|
19
|
+
| `sharedMaxIds` | 4096 | Retained request IDs per virtual session. |
|
|
20
|
+
| `sharedMaxCursors` | 256 | Retained tools-list cursors per virtual session. |
|
|
21
|
+
| `sharedMaxLineBytes` | 1048576 | Maximum JSON-line size, including translated fields. |
|
|
22
|
+
| `sharedMaxBufferBytes` | 4194304 | Maximum buffered protocol data per stream. |
|
|
23
|
+
|
|
24
|
+
All settings require bounded integers; invalid values fail explicitly. The bridge's existing
|
|
25
|
+
session cap and request/initialization timeouts also apply. A slot is reserved before waiting
|
|
26
|
+
for shared initialization or the spawn gate. Concurrent resumes of one external session ID
|
|
27
|
+
share one handshake.
|
|
28
|
+
Client and upstream JSON have a maximum depth of 64, counting the root as depth 0.
|
|
29
|
+
An upstream depth violation terminates its generation with explicit failure metadata,
|
|
30
|
+
not an uncaught serializer exception; no business call is replayed.
|
|
31
|
+
|
|
32
|
+
## Purpose and approval
|
|
33
|
+
|
|
34
|
+
`sharing: "shared"` reuses one initialized stdio child across compatible virtual HTTP sessions.
|
|
35
|
+
This differs from `pool`: a pooled child is uninitialized and belongs exclusively to its first
|
|
36
|
+
session. Both modes remain opt-in. No latency recommendation enables either mode automatically.
|
|
37
|
+
|
|
38
|
+
Shared mode is for a stateless-tools profile: tools operate on explicit inputs, the configured
|
|
39
|
+
credential context is common, and the server has no implicit per-client workspace, conversation,
|
|
40
|
+
account or subscription state. Enabling the mode asserts that profile. Matching capabilities
|
|
41
|
+
alone does not prove the implementation is stateless.
|
|
42
|
+
|
|
43
|
+
The default remains `isolated`. Unsupported clients receive a clear compatibility error before
|
|
44
|
+
their business request is dispatched. There is no silent sharing downgrade, guessed callback
|
|
45
|
+
recipient, or automatic replay on another process.
|
|
46
|
+
|
|
47
|
+
## Compatibility and initialization
|
|
48
|
+
|
|
49
|
+
One group per configured server and configuration generation is allowed. Its compatibility key
|
|
50
|
+
contains the complete, canonically ordered initialization parameters and configuration. Preserve
|
|
51
|
+
array order, absent properties and all client identity fields. The JSON-RPC request ID is not
|
|
52
|
+
part of the initialization parameters.
|
|
53
|
+
|
|
54
|
+
For the first profile:
|
|
55
|
+
|
|
56
|
+
- Support released protocol versions `2025-03-26`, `2025-06-18` and `2025-11-25`.
|
|
57
|
+
- Require empty client capabilities. Roots, sampling, elicitation, tasks and unknown extensions
|
|
58
|
+
cannot safely be routed from an arbitrary stdio callback to an individual client.
|
|
59
|
+
- Support `tools/list`, `tools/call` and `ping`.
|
|
60
|
+
- Reject stateful protocol methods, subscriptions, logging changes and task-augmented calls.
|
|
61
|
+
- Do not interpret arbitrary tool argument/result fields as request IDs or state handles.
|
|
62
|
+
- Bind tools-list cursors to their originating virtual session and child generation.
|
|
63
|
+
- Retain the actual upstream protocol version, instructions and implementation metadata.
|
|
64
|
+
The upstream may negotiate any of the three supported versions.
|
|
65
|
+
- Expose only the upstream's actual `tools` capability, without inventing flags. This
|
|
66
|
+
explicitly tools-only surface does not advertise optional prompts, resources, logging,
|
|
67
|
+
completions or tasks even if the upstream supports them. An empty `experimental` object
|
|
68
|
+
is harmless; unknown or nonempty experimental extensions are incompatible.
|
|
69
|
+
|
|
70
|
+
Concurrent compatible arrivals share a creation promise installed before awaiting process
|
|
71
|
+
creation. Exactly one upstream `initialize` and one `notifications/initialized` are sent.
|
|
72
|
+
Each virtual session receives the result with its own request ID and must complete its own
|
|
73
|
+
initialized notification before dispatching tools.
|
|
74
|
+
|
|
75
|
+
An incompatible client does not replace or disturb the established group. Failed initialization
|
|
76
|
+
fails all waiters, removes the failed creation promise, and terminates the candidate. A later
|
|
77
|
+
attempt may start a new group with bounded retry behavior.
|
|
78
|
+
|
|
79
|
+
## Routing
|
|
80
|
+
|
|
81
|
+
Use distinct typed keys: integer `1` and string `"1"` are different client IDs.
|
|
82
|
+
|
|
83
|
+
| Message | Required behavior |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Client request | Translate to a generation-unique upstream ID and retain owner plus original ID. |
|
|
86
|
+
| Child response | Deliver only to its mapped owner, restoring the original ID. |
|
|
87
|
+
| Client progress token | Translate only `_meta.progressToken`; restore it on the owner's progress notifications. |
|
|
88
|
+
| Client cancellation | Translate only that session's request ID. Never cancel another client's request. |
|
|
89
|
+
| Child `ping` | Answer as the bridge peer. It is not a response even if its ID matches an operation. |
|
|
90
|
+
| Other child requests | Reject explicitly; never select the first/recent client or broadcast callbacks. |
|
|
91
|
+
| Tools-list changes | Fan out to members only when the common catalog contract permits it. |
|
|
92
|
+
| Other notifications | Drop unowned logs and unexposed catalog changes with diagnostics. Never broadcast their data; reject unknown extensions. |
|
|
93
|
+
|
|
94
|
+
Classify messages by category (`method`, `result`, `error`) before consulting ID maps. Unknown
|
|
95
|
+
or late responses must not become notifications. Progress associated with a POST must be
|
|
96
|
+
delivered on its originating POST stream; an unrelated GET stream is not a replacement.
|
|
97
|
+
|
|
98
|
+
Bound operations, queued work, input lines, retained IDs/cursors and stream buffers. Reject
|
|
99
|
+
overflow explicitly instead of allocating without limit or silently truncating protocol data.
|
|
100
|
+
|
|
101
|
+
## Virtual child adapter
|
|
102
|
+
|
|
103
|
+
`bin/shared-sessions.mjs` exports `SharedSessionManager`. It takes:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
{
|
|
107
|
+
spawn: async (name, definition) => actualChild,
|
|
108
|
+
kill: (actualChild) => {},
|
|
109
|
+
onFailure: (name, detail) => {},
|
|
110
|
+
onInitialized: (name, elapsedMs) => {},
|
|
111
|
+
onResponse: (name) => {},
|
|
112
|
+
log: (name, text) => {},
|
|
113
|
+
initTimeoutMs,
|
|
114
|
+
requestTimeoutMs
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`await manager.acquire(name, definition, initializeParams)` returns an EventEmitter-compatible
|
|
119
|
+
virtual child with `stdin` accepting newline-delimited JSON-RPC, readable `stdout`, the actual
|
|
120
|
+
group `pid`, `exitCode`, `signalCode`, `__spawnedAt`, `__sharedSession: true`, and `detach()`.
|
|
121
|
+
It plugs into the existing Streamable HTTP per-session response parser without spawning again.
|
|
122
|
+
One actual child owns one stdout parser; virtual stdout contains only that session's messages.
|
|
123
|
+
For already-parsed HTTP JSON, use `child.writeMessage(message)`. It applies the same input,
|
|
124
|
+
depth, ID and ownership guards before serialization. Calling it after detachment throws an
|
|
125
|
+
explicit `SHARED_DETACHED` admission error instead of leaving a request waiting indefinitely.
|
|
126
|
+
|
|
127
|
+
The bridge's process-kill helper must detect a virtual child and detach it instead of killing
|
|
128
|
+
its shared PID. Actual group creation goes through the existing spawn gate and B21 counter.
|
|
129
|
+
Virtual session attachment is not an OS spawn or a cold-initialization sample.
|
|
130
|
+
`onResponse` reports actual upstream responses, not locally generated rejections. Health must
|
|
131
|
+
not classify errors by a numeric JSON-RPC code that an upstream can also legitimately use.
|
|
132
|
+
|
|
133
|
+
Provide `recycle(name)`, `remove(name)`, `shutdown()` and `inspect(name)` methods. Inspection
|
|
134
|
+
exposes only lifecycle facts such as generation, state, members and unresolved operations.
|
|
135
|
+
|
|
136
|
+
## Lifetime, drain and credentials
|
|
137
|
+
|
|
138
|
+
Deleting one session forgets and tombstones only that session. Remove its unsent work and send
|
|
139
|
+
owner-scoped cancellation for dispatched work. Other clients and their operations remain alive.
|
|
140
|
+
The actual child lingers between compatible sessions, bounded by `sharedLingerMs` (default
|
|
141
|
+
30 minutes). Unresolved work prevents idle eviction.
|
|
142
|
+
|
|
143
|
+
Cancellation is not proof that execution stopped. Retain execution ownership until the
|
|
144
|
+
response arrives or a bounded failure/drain policy terminates the generation.
|
|
145
|
+
|
|
146
|
+
Recycling marks the generation draining and stops new dispatch to it. Existing work gets a
|
|
147
|
+
bounded opportunity to finish; once drained, terminate promptly rather than waiting for the
|
|
148
|
+
next full recycle interval. A drain deadline fails unresolved work explicitly as potentially
|
|
149
|
+
executed, terminates that generation, and permits a later fresh generation. Never replay those
|
|
150
|
+
business calls. Fence old exit handlers and timers so they cannot delete a replacement.
|
|
151
|
+
|
|
152
|
+
The server retains its existing credential mechanism. The bridge does not freeze a token in
|
|
153
|
+
the shared initialization, infer renewal from a generic cache, or substitute an auth provider.
|
|
154
|
+
Keep configured credential recycling. A synthetic short-expiry test must show: expired call
|
|
155
|
+
fails unchanged; draining/replacement creates a fresh generation; a new call succeeds; the
|
|
156
|
+
failed call was dispatched once. Real provider renewal still requires its own canary.
|
|
157
|
+
|
|
158
|
+
Resume reattaches only compatible stateless sessions. It is not restoration of arbitrary
|
|
159
|
+
application state and does not preserve TCP connections across a bridge restart; that is B14.
|
|
160
|
+
|
|
161
|
+
## Acceptance gates
|
|
162
|
+
|
|
163
|
+
1. More than fifty compatible initialize/DELETE cycles reuse one actual child and one upstream
|
|
164
|
+
initialization. Two simultaneously active sessions must also share that same generation.
|
|
165
|
+
2. Concurrent numeric/string ID collisions, out-of-order results, progress and cancellation
|
|
166
|
+
route to the correct owner. Forged or late messages cannot cross sessions.
|
|
167
|
+
3. Delete one client while another works, kill a child during a request, and recycle while busy.
|
|
168
|
+
Pending callers fail promptly and dispatched tool calls are never replayed.
|
|
169
|
+
4. Incompatible initialization and unsupported callbacks/methods fail explicitly. Isolation
|
|
170
|
+
defaults and existing HTTP proxy behavior remain unchanged.
|
|
171
|
+
5. Counter deltas for identical isolated and shared workloads show actual reduced launches,
|
|
172
|
+
not merely fewer latency samples or a lower instant process count.
|
|
173
|
+
6. Use an isolated config directory and spare port for real-server and browser/CLI canaries.
|
|
174
|
+
Do not alter another project's client or orchestration configuration.
|
|
175
|
+
|
|
176
|
+
Reference: released MCP
|
|
177
|
+
[lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle),
|
|
178
|
+
[transports](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports),
|
|
179
|
+
[progress](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress), and
|
|
180
|
+
[cancellation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation).
|
|
181
|
+
Draft transport changes are not applied to clients speaking released versions.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"filesystem": {
|
|
3
|
+
"command": "npx",
|
|
4
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
5
|
+
},
|
|
6
|
+
"github": {
|
|
7
|
+
"command": "npx",
|
|
8
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
9
|
+
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "REPLACE_WITH_YOUR_PAT" }
|
|
10
|
+
},
|
|
11
|
+
"example-http-static-token": {
|
|
12
|
+
"type": "http",
|
|
13
|
+
"url": "https://example.com/mcp",
|
|
14
|
+
"auth": { "type": "env", "var": "EXAMPLE_TOKEN" }
|
|
15
|
+
},
|
|
16
|
+
"example-http-cli-token": {
|
|
17
|
+
"type": "http",
|
|
18
|
+
"url": "https://your-service.example.com/mcp",
|
|
19
|
+
"auth": {
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "az account get-access-token --resource api://your-resource-id --query accessToken -o tsv",
|
|
22
|
+
"refreshMinutes": 50
|
|
23
|
+
},
|
|
24
|
+
"headers": { "x-example-header": "value" }
|
|
25
|
+
},
|
|
26
|
+
"example-http-no-auth": {
|
|
27
|
+
"type": "http",
|
|
28
|
+
"url": "https://open.example.com/mcp",
|
|
29
|
+
"auth": { "type": "none" }
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-pacemaker",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Keeps your MCP servers alive across editor restarts, reboots, and token expiry — pluggable auth, cross-platform auto-start, one-command setup. Zero-dependency bridge; a small CLI for setup.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-pacemaker": "bin/cli.mjs",
|
|
8
|
+
"mcp-bridge": "bin/mcp-bridge.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"autostart/",
|
|
13
|
+
"supervisor/",
|
|
14
|
+
"tools/windows-security-helper/",
|
|
15
|
+
"examples/",
|
|
16
|
+
"ui/dist/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"SECURITY.md",
|
|
20
|
+
"docs/shared-sessions.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"THIRD_PARTY_NOTICES.txt",
|
|
23
|
+
"CONTRIBUTING.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "Girish Konda",
|
|
30
|
+
"homepage": "https://github.com/girishkvs/mcp-pacemaker#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/girishkvs/mcp-pacemaker/issues"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/girishkvs/mcp-pacemaker.git"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"mcp",
|
|
40
|
+
"model-context-protocol",
|
|
41
|
+
"sse",
|
|
42
|
+
"bridge",
|
|
43
|
+
"proxy",
|
|
44
|
+
"vscode",
|
|
45
|
+
"cursor",
|
|
46
|
+
"claude-desktop",
|
|
47
|
+
"supervisor",
|
|
48
|
+
"keep-alive"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"bridge": "node bin/mcp-bridge.mjs",
|
|
52
|
+
"cli": "node bin/cli.mjs",
|
|
53
|
+
"consumer:check": "node tools/npm-consumer/check.mjs",
|
|
54
|
+
"compat:prepare": "node tools/compatibility/prepare.mjs",
|
|
55
|
+
"compat:prepare:baseline": "node tools/compatibility/prepare.mjs --historical",
|
|
56
|
+
"compat:clean": "node tools/compatibility/prepare.mjs --clean",
|
|
57
|
+
"test:compat": "node --test --test-concurrency=1 test/compat/gates.test.mjs test/compat/cli-api.test.mjs",
|
|
58
|
+
"test:compat:browser": "node --test --test-concurrency=1 test/compat/browser.test.mjs",
|
|
59
|
+
"test": "node --test test/bridge.test.mjs test/bridge-controls.test.mjs test/kill-tree.test.mjs test/keepalive.test.mjs test/auth-token.test.mjs test/cold-start.test.mjs test/crash-report.test.mjs test/resume.test.mjs test/config-checks.test.mjs test/config-reload.test.mjs test/health.test.mjs test/cli.test.mjs test/logs.test.mjs test/prewarm.test.mjs test/pooling-batch-scheduler.test.mjs test/pooling-batch-api.test.mjs test/pooling-batch-store.test.mjs test/pooling-batches.test.mjs test/pooling-stage-publication.test.mjs test/pooling-batch-startup.test.mjs test/pooling-postcommit-recovery.test.mjs test/pooling-config.test.mjs test/pooling-writer.test.mjs test/pooling-api.test.mjs test/pooling-deadlines.test.mjs test/pooling-queue-budget.test.mjs test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs test/tui.test.mjs test/warm-pool.test.mjs test/server-metrics.test.mjs test/admission.test.mjs test/shared-sessions.test.mjs test/shared-http.test.mjs test/cross-host.test.mjs test/suite-integrity.test.mjs test/consumer-package.test.mjs test/notices.test.mjs test/notices-runtime.test.mjs test/npm-publication.test.mjs test/npm-publication-proof.test.mjs test/update-check.test.mjs test/service-lifecycle.test.mjs test/service-supervisor.test.mjs test/npm-publication-gates.test.mjs test/npm-publication-matrix.test.mjs test/publication-scanners-advisories.test.mjs test/publication-scanners.test.mjs test/service-replacement.test.mjs test/consumer-environment.test.mjs test/npm-publication-external.test.mjs test/npm-publication-peer.test.mjs test/publication-scanners-diagnostics.test.mjs test/publication-scanners-publication.test.mjs test/publication-scanners-paths.test.mjs test/npm-publication-local-enforcement.test.mjs test/publication-scanners-admission.test.mjs",
|
|
60
|
+
"notices:check": "node tools/third-party-notices/check.mjs",
|
|
61
|
+
"publication:source-gates": "node tools/npm-publication/source-gates.mjs",
|
|
62
|
+
"publication:artifact-gates": "node tools/npm-publication/artifact-gates.mjs",
|
|
63
|
+
"service:replacement:check": "node tools/service-replacement/check.mjs",
|
|
64
|
+
"publication:external-gates": "node tools/npm-publication/external-gates.mjs",
|
|
65
|
+
"publication:local-gate": "node tools/npm-publication/local-windows.mjs",
|
|
66
|
+
"publication:verify-local": "node tools/npm-publication/verify-local.mjs"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@clack/prompts": "^1.7.0",
|
|
70
|
+
"commander": "^15.0.0",
|
|
71
|
+
"ink": "^7.1.1",
|
|
72
|
+
"picocolors": "^1.1.1",
|
|
73
|
+
"react": "^19.2.7",
|
|
74
|
+
"semver": "^7.7.3",
|
|
75
|
+
"smol-toml": "^1.8.0"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// IPC reaches the bridge's existing graceful shutdown handler on Windows too.
|
|
2
|
+
let loaded = false;
|
|
3
|
+
let stopRequested = false;
|
|
4
|
+
function shutdown() {
|
|
5
|
+
stopRequested = true;
|
|
6
|
+
if (loaded) process.emit('SIGTERM');
|
|
7
|
+
}
|
|
8
|
+
process.on('message', (message) => {
|
|
9
|
+
if (message === 'stop') shutdown();
|
|
10
|
+
});
|
|
11
|
+
process.on('disconnect', shutdown);
|
|
12
|
+
await import('../bin/mcp-bridge.mjs');
|
|
13
|
+
loaded = true;
|
|
14
|
+
if (stopRequested) shutdown();
|