@rulvar/core 1.168.0 → 1.169.0

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/dist/index.d.ts CHANGED
@@ -10809,6 +10809,36 @@ interface McpConfig {
10809
10809
  approval?: boolean | Record<string, boolean>;
10810
10810
  /** Host-supplied risk labels for imported tools. */
10811
10811
  risk?: Record<string, ToolRisk>;
10812
+ /**
10813
+ * Cap on WIRE tools accepted from the tools/list sweep (RV1515),
10814
+ * checked after each page, PRE-filter: the sweep itself is the
10815
+ * resource being bounded, so allow/deny cannot admit past it. A
10816
+ * server that streams more refuses typed. Positive integer; absent =
10817
+ * unbounded (today's behavior).
10818
+ */
10819
+ maxTools?: number;
10820
+ /**
10821
+ * Per ADMITTED tool (allow/deny filter first): the UTF-8 byte length
10822
+ * of the serialized inputSchema plus outputSchema when present
10823
+ * (RV1515). An oversized tool refuses the resolution typed, naming
10824
+ * the tool and its measured bytes; deny the tool or raise the cap.
10825
+ * Positive integer; absent = unbounded.
10826
+ */
10827
+ maxSchemaBytes?: number;
10828
+ /**
10829
+ * Per-source latency bounds (RV1515). connectMs races the transport
10830
+ * handshake (on expiry the client, and for stdio its child, is
10831
+ * released and the refusal is typed). listMs and callMs ride the SDK
10832
+ * request timeout per tools/list page and per tools/call; without
10833
+ * them the SDK's own 60s default request timeout applies. A call
10834
+ * timeout surfaces as the tool's error result, never past policy.
10835
+ * Each a positive finite number of milliseconds.
10836
+ */
10837
+ timeouts?: {
10838
+ connectMs?: number;
10839
+ listMs?: number;
10840
+ callMs?: number;
10841
+ };
10812
10842
  }
10813
10843
  /**
10814
10844
  * The ToolSource returned by {@link mcp}: the frozen ToolSource seam
package/dist/index.js CHANGED
@@ -3879,7 +3879,24 @@ function buildToolContext(seed) {
3879
3879
  *
3880
3880
  * Docs: https://docs.rulvar.com/guide/mcp.
3881
3881
  */
3882
+ function validateBounds(cfg) {
3883
+ const positiveInt = (key) => {
3884
+ const value = cfg[key];
3885
+ if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) throw new ConfigError(`mcp: '${key}' must be a positive integer, got ${String(value)}`);
3886
+ };
3887
+ positiveInt("maxTools");
3888
+ positiveInt("maxSchemaBytes");
3889
+ for (const key of [
3890
+ "connectMs",
3891
+ "listMs",
3892
+ "callMs"
3893
+ ]) {
3894
+ const value = cfg.timeouts?.[key];
3895
+ if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) throw new ConfigError(`mcp: 'timeouts.${key}' must be a positive finite number of milliseconds, got ${String(value)}`);
3896
+ }
3897
+ }
3882
3898
  function validateConfig(cfg) {
3899
+ validateBounds(cfg);
3883
3900
  const forbid = (key) => {
3884
3901
  if (cfg[key] !== void 0) throw new ConfigError(`mcp: '${key}' is not a config key of the '${cfg.transport}' transport (exactly the keys matching the chosen transport)`);
3885
3902
  };
@@ -3947,7 +3964,7 @@ function mcp(cfg) {
3947
3964
  name: "rulvar",
3948
3965
  version: "1.0.0"
3949
3966
  });
3950
- try {
3967
+ const attach = async () => {
3951
3968
  if (cfg.transport === "stdio") {
3952
3969
  const transport = new StdioClientTransport({
3953
3970
  command: cfg.command ?? "",
@@ -3962,6 +3979,23 @@ function mcp(cfg) {
3962
3979
  await cfg.server.connect(serverTransport);
3963
3980
  await client.connect(clientTransport);
3964
3981
  }
3982
+ };
3983
+ try {
3984
+ const budgetMs = cfg.timeouts?.connectMs;
3985
+ if (budgetMs === void 0) await attach();
3986
+ else {
3987
+ let timer;
3988
+ const expired = new Promise((_resolve, reject) => {
3989
+ timer = setTimeout(() => {
3990
+ reject(new ConfigError(`mcp: connect to '${sourceIdOf(cfg)}' timed out after ${budgetMs}ms`));
3991
+ }, budgetMs);
3992
+ });
3993
+ try {
3994
+ await Promise.race([attach(), expired]);
3995
+ } finally {
3996
+ clearTimeout(timer);
3997
+ }
3998
+ }
3965
3999
  } catch (error) {
3966
4000
  await client.close().catch(() => void 0);
3967
4001
  throw error;
@@ -3975,13 +4009,20 @@ function mcp(cfg) {
3975
4009
  const listAll = async (client) => {
3976
4010
  const tools = [];
3977
4011
  let cursor;
4012
+ const listOptions = cfg.timeouts?.listMs === void 0 ? void 0 : { timeout: cfg.timeouts.listMs };
3978
4013
  do {
3979
- const page = await client.listTools(cursor === void 0 ? {} : { cursor });
4014
+ const page = await client.listTools(cursor === void 0 ? {} : { cursor }, listOptions);
3980
4015
  tools.push(...page.tools);
4016
+ if (cfg.maxTools !== void 0 && tools.length > cfg.maxTools) throw new ConfigError(`mcp: tools/list of '${sourceIdOf(cfg)}' returned at least ${tools.length} wire tools, over the declared maxTools ${cfg.maxTools}; raise the cap or trim the server`);
3981
4017
  cursor = page.nextCursor;
3982
4018
  } while (cursor !== void 0 && cursor !== "");
3983
4019
  return tools;
3984
4020
  };
4021
+ const enforceSchemaBytes = (wire) => {
4022
+ if (cfg.maxSchemaBytes === void 0) return;
4023
+ const bytes = Buffer.byteLength(JSON.stringify(wire.inputSchema) + (wire.outputSchema === void 0 ? "" : JSON.stringify(wire.outputSchema)), "utf8");
4024
+ if (bytes > cfg.maxSchemaBytes) throw new ConfigError(`mcp: tool '${wire.name}' declares ${bytes} bytes of schema, over the declared maxSchemaBytes ${cfg.maxSchemaBytes}; deny the tool or raise the cap`);
4025
+ };
3985
4026
  const needsApprovalFor = (originalName) => {
3986
4027
  if (cfg.approval === void 0) return false;
3987
4028
  if (typeof cfg.approval === "boolean") return cfg.approval;
@@ -4001,7 +4042,7 @@ function mcp(cfg) {
4001
4042
  const result = await client.callTool({
4002
4043
  name: wire.name,
4003
4044
  arguments: input ?? {}
4004
- });
4045
+ }, void 0, cfg.timeouts?.callMs === void 0 ? void 0 : { timeout: cfg.timeouts.callMs });
4005
4046
  if (result.isError === true) throw new Error(errorText(result));
4006
4047
  if (result.structuredContent !== void 0) {
4007
4048
  if (wire.outputSchema !== void 0) {
@@ -4026,7 +4067,9 @@ function mcp(cfg) {
4026
4067
  const wireTools = await listAll(client);
4027
4068
  const denySet = new Set(cfg.deny ?? []);
4028
4069
  const allowSet = cfg.allow === void 0 ? void 0 : new Set(cfg.allow);
4029
- const defs = wireTools.filter((wire) => !denySet.has(wire.name) && (allowSet === void 0 || allowSet.has(wire.name))).map((wire) => toDef(client, wire));
4070
+ const admitted = wireTools.filter((wire) => !denySet.has(wire.name) && (allowSet === void 0 || allowSet.has(wire.name)));
4071
+ for (const wire of admitted) enforceSchemaBytes(wire);
4072
+ const defs = admitted.map((wire) => toDef(client, wire));
4030
4073
  if (generation === fetchedAt) cache = defs;
4031
4074
  return defs;
4032
4075
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.168.0",
3
+ "version": "1.169.0",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",