stdio.js 6.7 KB

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