stdio.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import spawn from 'cross-spawn';
  2. import process from 'node:process';
  3. import { PassThrough } from 'node:stream';
  4. import { ReadBuffer, serializeMessage } from '../shared/stdio.js';
  5. /**
  6. * Environment variables to inherit by default, if an environment is not explicitly given.
  7. */
  8. export const DEFAULT_INHERITED_ENV_VARS = process.platform === 'win32'
  9. ? [
  10. 'APPDATA',
  11. 'HOMEDRIVE',
  12. 'HOMEPATH',
  13. 'LOCALAPPDATA',
  14. 'PATH',
  15. 'PROCESSOR_ARCHITECTURE',
  16. 'SYSTEMDRIVE',
  17. 'SYSTEMROOT',
  18. 'TEMP',
  19. 'USERNAME',
  20. 'USERPROFILE',
  21. 'PROGRAMFILES'
  22. ]
  23. : /* list inspired by the default env inheritance of sudo */
  24. ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
  25. /**
  26. * Returns a default environment object including only environment variables deemed safe to inherit.
  27. */
  28. export function getDefaultEnvironment() {
  29. const env = {};
  30. for (const key of DEFAULT_INHERITED_ENV_VARS) {
  31. const value = process.env[key];
  32. if (value === undefined) {
  33. continue;
  34. }
  35. if (value.startsWith('()')) {
  36. // Skip functions, which are a security risk.
  37. continue;
  38. }
  39. env[key] = value;
  40. }
  41. return env;
  42. }
  43. /**
  44. * Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout.
  45. *
  46. * This transport is only available in Node.js environments.
  47. */
  48. export class StdioClientTransport {
  49. constructor(server) {
  50. this._readBuffer = new ReadBuffer();
  51. this._stderrStream = null;
  52. this._serverParams = server;
  53. if (server.stderr === 'pipe' || server.stderr === 'overlapped') {
  54. this._stderrStream = new PassThrough();
  55. }
  56. }
  57. /**
  58. * Starts the server process and prepares to communicate with it.
  59. */
  60. async start() {
  61. if (this._process) {
  62. throw new Error('StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.');
  63. }
  64. return new Promise((resolve, reject) => {
  65. this._process = spawn(this._serverParams.command, this._serverParams.args ?? [], {
  66. // merge default env with server env because mcp server needs some env vars
  67. env: {
  68. ...getDefaultEnvironment(),
  69. ...this._serverParams.env
  70. },
  71. stdio: ['pipe', 'pipe', this._serverParams.stderr ?? 'inherit'],
  72. shell: false,
  73. windowsHide: process.platform === 'win32',
  74. cwd: this._serverParams.cwd
  75. });
  76. this._process.on('error', error => {
  77. reject(error);
  78. this.onerror?.(error);
  79. });
  80. this._process.on('spawn', () => {
  81. resolve();
  82. });
  83. this._process.on('close', _code => {
  84. this._process = undefined;
  85. this.onclose?.();
  86. });
  87. this._process.stdin?.on('error', error => {
  88. this.onerror?.(error);
  89. });
  90. this._process.stdout?.on('data', chunk => {
  91. this._readBuffer.append(chunk);
  92. this.processReadBuffer();
  93. });
  94. this._process.stdout?.on('error', error => {
  95. this.onerror?.(error);
  96. });
  97. if (this._stderrStream && this._process.stderr) {
  98. this._process.stderr.pipe(this._stderrStream);
  99. }
  100. });
  101. }
  102. /**
  103. * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped".
  104. *
  105. * If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to
  106. * attach listeners before the start method is invoked. This prevents loss of any early
  107. * error output emitted by the child process.
  108. */
  109. get stderr() {
  110. if (this._stderrStream) {
  111. return this._stderrStream;
  112. }
  113. return this._process?.stderr ?? null;
  114. }
  115. /**
  116. * The child process pid spawned by this transport.
  117. *
  118. * This is only available after the transport has been started.
  119. */
  120. get pid() {
  121. return this._process?.pid ?? null;
  122. }
  123. processReadBuffer() {
  124. while (true) {
  125. try {
  126. const message = this._readBuffer.readMessage();
  127. if (message === null) {
  128. break;
  129. }
  130. this.onmessage?.(message);
  131. }
  132. catch (error) {
  133. this.onerror?.(error);
  134. }
  135. }
  136. }
  137. async close() {
  138. if (this._process) {
  139. const processToClose = this._process;
  140. this._process = undefined;
  141. const closePromise = new Promise(resolve => {
  142. processToClose.once('close', () => {
  143. resolve();
  144. });
  145. });
  146. try {
  147. processToClose.stdin?.end();
  148. }
  149. catch {
  150. // ignore
  151. }
  152. await Promise.race([closePromise, new Promise(resolve => setTimeout(resolve, 2000).unref())]);
  153. if (processToClose.exitCode === null) {
  154. try {
  155. processToClose.kill('SIGTERM');
  156. }
  157. catch {
  158. // ignore
  159. }
  160. await Promise.race([closePromise, new Promise(resolve => setTimeout(resolve, 2000).unref())]);
  161. }
  162. if (processToClose.exitCode === null) {
  163. try {
  164. processToClose.kill('SIGKILL');
  165. }
  166. catch {
  167. // ignore
  168. }
  169. }
  170. }
  171. this._readBuffer.clear();
  172. }
  173. send(message) {
  174. return new Promise(resolve => {
  175. if (!this._process?.stdin) {
  176. throw new Error('Not connected');
  177. }
  178. const json = serializeMessage(message);
  179. if (this._process.stdin.write(json)) {
  180. resolve();
  181. }
  182. else {
  183. this._process.stdin.once('drain', resolve);
  184. }
  185. });
  186. }
  187. }
  188. //# sourceMappingURL=stdio.js.map