@myspec/mcp-server 0.4.0-next.108 → 0.4.0-next.110

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.
Files changed (2) hide show
  1. package/dist/index.js +302 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32,10 +32,11 @@ var REFRESH_TOKEN_PATTERN = /"refreshToken"\s*:\s*"[^"]+"/g;
32
32
  var API_TOKEN_PATTERN = /"apiToken"\s*:\s*"[^"]+"/g;
33
33
  var API_TOKEN_VALUE_PATTERN = /\bmsp_pat_[A-Za-z0-9]+/g;
34
34
  var ACCESS_TOKEN_PATTERN = /"accessToken"\s*:\s*"[^"]+"/g;
35
- var SNAKE_TOKEN_PATTERN = /"(access_token|refresh_token|id_token)"\s*:\s*"[^"]+"/g;
35
+ var SNAKE_TOKEN_PATTERN = /"(access_token|refresh_token|id_token|token)"\s*:\s*"[^"]+"/g;
36
36
  var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
37
+ var STREAM_URL_PATTERN = /\/realtime\/v1\/stream\/[A-Za-z0-9_-]{20,}/g;
37
38
  function redactSensitive(value) {
38
- return value.replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(REFRESH_TOKEN_PATTERN, '"refreshToken":"[REDACTED]"').replace(API_TOKEN_PATTERN, '"apiToken":"[REDACTED]"').replace(API_TOKEN_VALUE_PATTERN, "[REDACTED_API_TOKEN]").replace(ACCESS_TOKEN_PATTERN, '"accessToken":"[REDACTED]"').replace(SNAKE_TOKEN_PATTERN, (_match, key) => `"${key}":"[REDACTED]"`).replace(JWT_PATTERN, "[REDACTED_JWT]");
39
+ return value.replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(REFRESH_TOKEN_PATTERN, '"refreshToken":"[REDACTED]"').replace(API_TOKEN_PATTERN, '"apiToken":"[REDACTED]"').replace(API_TOKEN_VALUE_PATTERN, "[REDACTED_API_TOKEN]").replace(ACCESS_TOKEN_PATTERN, '"accessToken":"[REDACTED]"').replace(SNAKE_TOKEN_PATTERN, (_match, key) => `"${key}":"[REDACTED]"`).replace(JWT_PATTERN, "[REDACTED_JWT]").replace(STREAM_URL_PATTERN, "/realtime/v1/stream/[REDACTED_STREAM_TOKEN]");
39
40
  }
40
41
  var ConfigError = class extends Error {
41
42
  constructor(message) {
@@ -2716,6 +2717,127 @@ var HttpArtifactClient = class {
2716
2717
  }
2717
2718
  };
2718
2719
 
2720
+ // ../../packages/platform-client/stream/types/stream-token.types.ts
2721
+ function optionalDate(value) {
2722
+ return value ? new Date(value) : null;
2723
+ }
2724
+ function transformStreamToken(raw) {
2725
+ return {
2726
+ id: raw.id,
2727
+ tokenPrefix: raw.token_prefix,
2728
+ label: raw.label ?? "",
2729
+ resourceType: raw.resource_type,
2730
+ resourceId: raw.resource_id,
2731
+ eventTypes: raw.event_types ?? [],
2732
+ createdBy: raw.created_by,
2733
+ createdAt: new Date(raw.created_at),
2734
+ expiresAt: new Date(raw.expires_at),
2735
+ revokedAt: optionalDate(raw.revoked_at),
2736
+ resourceDeletedAt: optionalDate(raw.resource_deleted_at),
2737
+ lastConnectedAt: optionalDate(raw.last_connected_at),
2738
+ connectCount: raw.connect_count,
2739
+ live: raw.live
2740
+ };
2741
+ }
2742
+ function transformStreamTokenMintResult(raw) {
2743
+ if (!raw.token || !raw.url) {
2744
+ throw new Error("stream token mint response carried no token or url");
2745
+ }
2746
+ return { ...transformStreamToken(raw), token: raw.token, url: raw.url };
2747
+ }
2748
+ function transformStreamTokenListResult(raw) {
2749
+ return {
2750
+ tokens: (raw.tokens ?? []).map(transformStreamToken),
2751
+ total: raw.total ?? 0
2752
+ };
2753
+ }
2754
+ function transformStreamCatalogue(raw) {
2755
+ return {
2756
+ resources: (raw.resources ?? []).map((resource) => ({
2757
+ resourceType: resource.resource_type,
2758
+ eventTypes: resource.event_types ?? []
2759
+ })),
2760
+ maxTtlSeconds: raw.max_ttl_seconds,
2761
+ defaultTtlSeconds: raw.default_ttl_seconds
2762
+ };
2763
+ }
2764
+ function buildCreateStreamTokenBody(request) {
2765
+ const body = {
2766
+ resource_type: request.resourceType,
2767
+ resource_id: request.resourceId
2768
+ };
2769
+ body.event_types = request.eventTypes;
2770
+ if (request.ttlSeconds !== void 0) {
2771
+ body.ttl_seconds = request.ttlSeconds;
2772
+ }
2773
+ if (request.label) {
2774
+ body.label = request.label;
2775
+ }
2776
+ return body;
2777
+ }
2778
+
2779
+ // ../../packages/platform-client/stream/http/stream-token.http-client.ts
2780
+ var BASE_PATH2 = "/realtime/v1";
2781
+ function requestOptions2(options) {
2782
+ return options ?? {};
2783
+ }
2784
+ var HttpStreamTokenClient = class {
2785
+ constructor(httpClient) {
2786
+ this.httpClient = httpClient;
2787
+ }
2788
+ httpClient;
2789
+ async createStreamToken(request, jwtToken, options) {
2790
+ const raw = await this.httpClient.post(
2791
+ `${BASE_PATH2}/stream-tokens`,
2792
+ buildCreateStreamTokenBody(request),
2793
+ jwtToken,
2794
+ requestOptions2(options)
2795
+ );
2796
+ return transformStreamTokenMintResult(raw);
2797
+ }
2798
+ async listStreamTokens(options, jwtToken, httpOptions) {
2799
+ const query = new URLSearchParams();
2800
+ if (options?.resourceId) {
2801
+ query.set("resource_id", options.resourceId);
2802
+ }
2803
+ if (options?.liveOnly) {
2804
+ query.set("live", "true");
2805
+ }
2806
+ if (options?.mineOnly) {
2807
+ query.set("mine", "true");
2808
+ }
2809
+ if (options?.limit !== void 0) {
2810
+ query.set("limit", String(options.limit));
2811
+ }
2812
+ if (options?.offset !== void 0) {
2813
+ query.set("offset", String(options.offset));
2814
+ }
2815
+ const suffix = query.toString() ? `?${query.toString()}` : "";
2816
+ const raw = await this.httpClient.get(
2817
+ `${BASE_PATH2}/stream-tokens${suffix}`,
2818
+ jwtToken,
2819
+ requestOptions2(httpOptions)
2820
+ );
2821
+ return transformStreamTokenListResult(raw);
2822
+ }
2823
+ async revokeStreamToken(id, jwtToken, options) {
2824
+ const raw = await this.httpClient.delete(
2825
+ `${BASE_PATH2}/stream-tokens/${encodeURIComponent(id)}`,
2826
+ jwtToken,
2827
+ requestOptions2(options)
2828
+ );
2829
+ return transformStreamToken(raw);
2830
+ }
2831
+ async getStreamCatalogue(jwtToken, options) {
2832
+ const raw = await this.httpClient.get(
2833
+ `${BASE_PATH2}/stream-catalogue`,
2834
+ jwtToken,
2835
+ requestOptions2(options)
2836
+ );
2837
+ return transformStreamCatalogue(raw);
2838
+ }
2839
+ };
2840
+
2719
2841
  // src/platform/client.ts
2720
2842
  var silentLogger = {
2721
2843
  debug: () => void 0,
@@ -3003,9 +3125,24 @@ var PlatformClient = class {
3003
3125
  session: new HttpSpecSessionClient(httpClient),
3004
3126
  file: new HttpFileClient(httpClient),
3005
3127
  attachment: new HttpAttachmentClient(httpClient),
3006
- artifact: new HttpArtifactClient(httpClient)
3128
+ artifact: new HttpArtifactClient(httpClient),
3129
+ streamToken: new HttpStreamTokenClient(httpClient)
3007
3130
  };
3008
3131
  }
3132
+ // ------------------------------------------------------- stream tokens
3133
+ //
3134
+ // A stream token is a presigned WebSocket URL for a project's event feed —
3135
+ // the URL is the whole credential, which is what lets a tool that takes one
3136
+ // string and nothing else consume the feed.
3137
+ async createStreamToken(request) {
3138
+ return this.withTokenRetry((jwt, c) => c.streamToken.createStreamToken(request, jwt));
3139
+ }
3140
+ async listStreamTokens(opts = {}) {
3141
+ return this.withTokenRetry((jwt, c) => c.streamToken.listStreamTokens(opts, jwt));
3142
+ }
3143
+ async revokeStreamToken(id) {
3144
+ return this.withTokenRetry((jwt, c) => c.streamToken.revokeStreamToken(id, jwt));
3145
+ }
3009
3146
  // ----------------------------------------------------------- artifacts
3010
3147
  //
3011
3148
  // Every write is attributed `actorKind: 'ai'`: an MCP client is an
@@ -5273,6 +5410,133 @@ var rollbackArtifactTool = {
5273
5410
  }
5274
5411
  };
5275
5412
 
5413
+ // src/server/tools/create_stream_token.ts
5414
+ import { z as z34 } from "zod";
5415
+
5416
+ // src/server/tools/stream-token-output.ts
5417
+ import { z as z33 } from "zod";
5418
+ var projectStreamEventTypes = [
5419
+ "project.updated",
5420
+ "project.deleted",
5421
+ "spec_file.created",
5422
+ "spec_file.updated",
5423
+ "spec_file.deleted",
5424
+ "spec_file.lock",
5425
+ "attachment.created",
5426
+ "attachment.deleted",
5427
+ "spec_session.created",
5428
+ "spec_session.updated",
5429
+ "spec_session.deleted",
5430
+ "*"
5431
+ ];
5432
+ var streamEventTypesSchema = z33.array(z33.enum(projectStreamEventTypes)).min(1).describe(
5433
+ 'Event types to receive. Required \u2014 pass ["*"] to follow all of them. spec_file.updated covers new revisions, rollbacks, and moves to or from the trash; spec_file.lock covers acquire, renew, release and takeover; spec_session.updated covers completion, archive, unarchive and rename.'
5434
+ );
5435
+ function streamTokenSummary(token) {
5436
+ return {
5437
+ id: token.id,
5438
+ token_prefix: token.tokenPrefix,
5439
+ label: token.label,
5440
+ resource_type: token.resourceType,
5441
+ resource_id: token.resourceId,
5442
+ event_types: token.eventTypes,
5443
+ created_by: token.createdBy,
5444
+ created_at: token.createdAt.toISOString(),
5445
+ expires_at: token.expiresAt.toISOString(),
5446
+ live: token.live,
5447
+ connect_count: token.connectCount,
5448
+ ...token.revokedAt ? { revoked_at: token.revokedAt.toISOString() } : {},
5449
+ ...token.resourceDeletedAt ? { resource_deleted_at: token.resourceDeletedAt.toISOString() } : {},
5450
+ ...token.lastConnectedAt ? { last_connected_at: token.lastConnectedAt.toISOString() } : {}
5451
+ };
5452
+ }
5453
+ function streamTokenMintOutput(result) {
5454
+ return {
5455
+ ...streamTokenSummary(result),
5456
+ url: result.url,
5457
+ note: "Treat this URL as a password: anyone who has it can read this project's events until it expires, with no further authentication. Pass it straight to a WebSocket client and do NOT write it to a file, a commit, an issue or a chat reply, and do not log or repeat it. It is shown once and cannot be retrieved again. If it may have been exposed, call revoke_stream_token with the id above. Open it with subprotocol 'v1'; each text frame is one JSON event, and reconnecting with ?after=<seq> resumes from the last frame you saw."
5458
+ };
5459
+ }
5460
+
5461
+ // src/server/tools/create_stream_token.ts
5462
+ var inputSchema32 = {
5463
+ resource_type: z34.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
5464
+ resource_id: z34.string().min(1).describe("UUID of the project to stream events for."),
5465
+ event_types: streamEventTypesSchema,
5466
+ ttl_seconds: z34.number().int().positive().optional().describe(
5467
+ "How long the URL stays valid. Defaults to 24 hours, capped at 7 days. Out-of-range values are clamped rather than rejected \u2014 check expires_at in the result."
5468
+ ),
5469
+ label: z34.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
5470
+ };
5471
+ var createStreamTokenTool = {
5472
+ name: "create_stream_token",
5473
+ description: "Create a short-lived WebSocket URL that streams a project's events in real time \u2014 like an S3 presigned URL, the returned URL is the whole credential and needs no auth header. Choose the project and the event types to follow; the result carries the full wss:// URL. Feed it to any WebSocket client with subprotocol 'v1' (for example Claude Code's Monitor tool). Each text frame is one JSON event carrying a `seq` cursor; reconnect with ?after=<seq> to resume without missing events. Defaults to 24 hours, 7 days maximum. TREAT THE RETURNED URL LIKE A PASSWORD: anyone holding it can read every event on this project until it expires, without authenticating. Pass it directly to the WebSocket client; never write it to a file, a commit, an issue or a chat reply, and never log or repeat it. It is shown once and cannot be retrieved again. If it may have been exposed, revoke it at once with revoke_stream_token \u2014 that is also how you end one early. Prefer the shortest ttl_seconds that covers the work.",
5474
+ inputSchema: inputSchema32,
5475
+ handler: async (args, ctx) => {
5476
+ const result = await ctx.client.createStreamToken({
5477
+ resourceType: args.resource_type,
5478
+ resourceId: args.resource_id,
5479
+ eventTypes: args.event_types,
5480
+ ttlSeconds: args.ttl_seconds,
5481
+ label: args.label
5482
+ });
5483
+ return jsonResult(streamTokenMintOutput(result));
5484
+ }
5485
+ };
5486
+
5487
+ // src/server/tools/list_stream_tokens.ts
5488
+ import { z as z35 } from "zod";
5489
+ var LIST_DEFAULT5 = 50;
5490
+ var LIST_MAX5 = 200;
5491
+ var inputSchema33 = {
5492
+ resource_id: z35.string().optional().describe("Only tokens following this project."),
5493
+ live_only: z35.boolean().optional().describe("Hide expired and revoked tokens."),
5494
+ mine_only: z35.boolean().optional().describe(
5495
+ "Only tokens you created. Off by default, so a leaked URL can be found \u2014 note that revoking one you did not mint needs the organisation owner or admin role."
5496
+ ),
5497
+ limit: z35.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
5498
+ offset: z35.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
5499
+ };
5500
+ var listStreamTokensTool = {
5501
+ name: "list_stream_tokens",
5502
+ description: "List the stream tokens in your organisation, with their scope, expiry and whether each is still live. Secrets are never returned \u2014 only a short prefix to tell tokens apart. Returns `total` and, when more remain, `next_offset`.",
5503
+ inputSchema: inputSchema33,
5504
+ handler: async (args, ctx) => {
5505
+ const limit = clampLimit(args.limit, LIST_DEFAULT5, LIST_MAX5);
5506
+ const offset = args.offset ?? 0;
5507
+ const page = await ctx.client.listStreamTokens({
5508
+ resourceId: args.resource_id,
5509
+ liveOnly: args.live_only,
5510
+ mineOnly: args.mine_only,
5511
+ limit,
5512
+ offset
5513
+ });
5514
+ const next = nextOffsetOf(offset, page.tokens.length, page.total);
5515
+ return jsonResult({
5516
+ tokens: page.tokens.map(streamTokenSummary),
5517
+ total: page.total,
5518
+ limit,
5519
+ offset,
5520
+ ...next === void 0 ? {} : { next_offset: next }
5521
+ });
5522
+ }
5523
+ };
5524
+
5525
+ // src/server/tools/revoke_stream_token.ts
5526
+ import { z as z36 } from "zod";
5527
+ var inputSchema34 = {
5528
+ id: z36.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
5529
+ };
5530
+ var revokeStreamTokenTool = {
5531
+ name: "revoke_stream_token",
5532
+ description: "Revoke a stream token immediately, closing its URL and deleting the events buffered for it. Use this as soon as a stream is no longer needed, or if its URL may have leaked \u2014 the URL is the credential, so revoking is the only way to stop it before it expires. Revoking a token you minted yourself always works. Revoking one somebody ELSE minted additionally requires the organisation owner or admin role, and is refused with 403 otherwise \u2014 so when hunting a leaked URL you did not create, expect that refusal and ask an organisation owner or admin rather than retrying.",
5533
+ inputSchema: inputSchema34,
5534
+ handler: async (args, ctx) => {
5535
+ const token = await ctx.client.revokeStreamToken(args.id);
5536
+ return jsonResult({ ...streamTokenSummary(token), revoked: true });
5537
+ }
5538
+ };
5539
+
5276
5540
  // src/server/server.ts
5277
5541
  function buildServer(deps) {
5278
5542
  const server = new McpServer({
@@ -5310,6 +5574,9 @@ function buildServer(deps) {
5310
5574
  registerTool(server, createArtifactTool, deps.client);
5311
5575
  registerTool(server, writeArtifactRevisionTool, deps.client);
5312
5576
  registerTool(server, rollbackArtifactTool, deps.client);
5577
+ registerTool(server, createStreamTokenTool, deps.client);
5578
+ registerTool(server, listStreamTokensTool, deps.client);
5579
+ registerTool(server, revokeStreamTokenTool, deps.client);
5313
5580
  return server;
5314
5581
  }
5315
5582
  async function startStdioServer(deps) {
@@ -5463,7 +5730,7 @@ async function canonicaliseRoot(root) {
5463
5730
  import { promises as fs4 } from "fs";
5464
5731
  import { join as join2, relative } from "path";
5465
5732
  import { spawn } from "child_process";
5466
- import { z as z33 } from "zod";
5733
+ import { z as z37 } from "zod";
5467
5734
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5468
5735
 
5469
5736
  // src/reverse/ignore.ts
@@ -5768,37 +6035,37 @@ var READ_FILE_MAX_LINES = 2e3;
5768
6035
  var LIST_DIR_HARD_CAP = 5e3;
5769
6036
  var LIST_DIR_DEFAULT_CAP = 1e3;
5770
6037
  var GREP_MAX_MATCHES_DEFAULT = 200;
5771
- var listDirInput = z33.object({
5772
- path: z33.string(),
5773
- recursive: z33.boolean().optional(),
5774
- maxEntries: z33.number().int().positive().optional(),
5775
- includeIgnored: z33.boolean().optional()
6038
+ var listDirInput = z37.object({
6039
+ path: z37.string(),
6040
+ recursive: z37.boolean().optional(),
6041
+ maxEntries: z37.number().int().positive().optional(),
6042
+ includeIgnored: z37.boolean().optional()
5776
6043
  });
5777
- var readFileInput = z33.object({
5778
- path: z33.string(),
5779
- offset: z33.number().int().nonnegative().optional(),
5780
- limit: z33.number().int().positive().optional(),
5781
- includeIgnored: z33.boolean().optional()
6044
+ var readFileInput = z37.object({
6045
+ path: z37.string(),
6046
+ offset: z37.number().int().nonnegative().optional(),
6047
+ limit: z37.number().int().positive().optional(),
6048
+ includeIgnored: z37.boolean().optional()
5782
6049
  });
5783
- var grepInput = z33.object({
5784
- pattern: z33.string().describe(
6050
+ var grepInput = z37.object({
6051
+ pattern: z37.string().describe(
5785
6052
  "Search pattern. Interpreted as a regular expression by default (ripgrep syntax), e.g. `foo|bar` or `[Rr]ole`. Set `isRegex: false` to match the pattern as an exact literal string instead."
5786
6053
  ),
5787
- path: z33.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
5788
- isRegex: z33.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
5789
- caseSensitive: z33.boolean().optional(),
5790
- maxMatches: z33.number().int().positive().optional(),
5791
- contextLines: z33.number().int().nonnegative().optional(),
5792
- includeIgnored: z33.boolean().optional()
6054
+ path: z37.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
6055
+ isRegex: z37.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
6056
+ caseSensitive: z37.boolean().optional(),
6057
+ maxMatches: z37.number().int().positive().optional(),
6058
+ contextLines: z37.number().int().nonnegative().optional(),
6059
+ includeIgnored: z37.boolean().optional()
5793
6060
  });
5794
- var packCodebaseInput = z33.object({
5795
- subpath: z33.string().optional(),
5796
- includePatterns: z33.string().optional(),
5797
- ignorePatterns: z33.string().optional()
6061
+ var packCodebaseInput = z37.object({
6062
+ subpath: z37.string().optional(),
6063
+ includePatterns: z37.string().optional(),
6064
+ ignorePatterns: z37.string().optional()
5798
6065
  });
5799
- var packCodebaseReadPageInput = z33.object({
5800
- outputId: z33.string(),
5801
- page: z33.number().int().positive()
6066
+ var packCodebaseReadPageInput = z37.object({
6067
+ outputId: z37.string(),
6068
+ page: z37.number().int().positive()
5802
6069
  });
5803
6070
  function structuredError(error, message, extra) {
5804
6071
  const payload = { error, message, ...extra ?? {} };
@@ -6326,7 +6593,7 @@ function jsonSchemaFromZod(schema) {
6326
6593
  return zodToJson(schema);
6327
6594
  }
6328
6595
  function zodToJson(schema) {
6329
- if (schema instanceof z33.ZodObject) {
6596
+ if (schema instanceof z37.ZodObject) {
6330
6597
  const shape = schema.shape;
6331
6598
  const properties = {};
6332
6599
  const required = [];
@@ -6341,19 +6608,19 @@ function zodToJson(schema) {
6341
6608
  if (required.length > 0) out.required = required;
6342
6609
  return out;
6343
6610
  }
6344
- if (schema instanceof z33.ZodOptional) {
6611
+ if (schema instanceof z37.ZodOptional) {
6345
6612
  return zodToJson(schema.unwrap());
6346
6613
  }
6347
- if (schema instanceof z33.ZodString) {
6614
+ if (schema instanceof z37.ZodString) {
6348
6615
  return { type: "string" };
6349
6616
  }
6350
- if (schema instanceof z33.ZodNumber) {
6617
+ if (schema instanceof z37.ZodNumber) {
6351
6618
  return { type: "number" };
6352
6619
  }
6353
- if (schema instanceof z33.ZodBoolean) {
6620
+ if (schema instanceof z37.ZodBoolean) {
6354
6621
  return { type: "boolean" };
6355
6622
  }
6356
- if (schema instanceof z33.ZodEnum) {
6623
+ if (schema instanceof z37.ZodEnum) {
6357
6624
  return { type: "string", enum: schema.options };
6358
6625
  }
6359
6626
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.4.0-next.108",
3
+ "version": "0.4.0-next.110",
4
4
  "description": "MySpec MCP server — exposes MySpec platform projects, files and attachments to MCP-aware clients via OAuth-authenticated access tokens.",
5
5
  "type": "module",
6
6
  "repository": {