@hasna/shortlinks 0.2.0 → 0.2.2

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.
@@ -1,21 +1,26 @@
1
1
  import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { type McpHttpServerHandle } from "@hasna/mcp-harness";
3
2
  /**
4
- * open-shortlinks MCP transport/port boilerplate — thin shim over
5
- * `@hasna/mcp-harness`. The public API (names, signatures, health shape) is
6
- * preserved so `mcp/index.ts` is unchanged; only the hand-rolled `node:http` +
7
- * `StreamableHTTPServerTransport` server, port parsing, and health helpers
8
- * were removed in favor of the shared harness.
3
+ * open-shortlinks MCP transport/port boilerplate — self-contained, with NO
4
+ * external harness dependency. The prior version depended on the unpublished
5
+ * `@hasna/mcp-harness` (a `file:` link), which made `shortlinks-mcp`
6
+ * unstartable on a fresh `npm i` / `bun add -g`. This version inlines the tiny
7
+ * amount of transport glue using only the published `@modelcontextprotocol/sdk`
8
+ * and Bun's built-in HTTP server, matching the pattern used by the reference
9
+ * apps (open-conversations, open-mementos).
9
10
  *
10
- * shortlinks builds its MCP server with the low-level `Server` (not the
11
- * high-level `McpServer`); the harness's Node adapter is typed against
12
- * `McpServer` but only calls `.connect()`/`.close()` at runtime, both of
13
- * which `Server` implements identically — the cast below mirrors the same
14
- * pattern used by open-signatures.
11
+ * shortlinks builds its MCP server with the low-level `Server`; the WebStandard
12
+ * transport's `.connect()` works identically on `Server` and the high-level
13
+ * `McpServer`.
15
14
  */
16
15
  export declare const MCP_HTTP_SERVICE_NAME = "shortlinks";
16
+ export declare const MCP_HTTP_HOST = "127.0.0.1";
17
17
  export declare const DEFAULT_MCP_HTTP_PORT = 8851;
18
- export type { McpHttpServerHandle };
18
+ export interface McpHttpServerHandle {
19
+ port: number;
20
+ host: string;
21
+ url: string;
22
+ close(): Promise<void>;
23
+ }
19
24
  export declare function isHttpMode(argv?: string[], env?: NodeJS.ProcessEnv): boolean;
20
25
  export declare function isStdioMode(argv?: string[], env?: NodeJS.ProcessEnv): boolean;
21
26
  export declare function resolveMcpHttpPort(argv?: string[], env?: NodeJS.ProcessEnv): number;
package/dist/mcp/index.js CHANGED
@@ -38,7 +38,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
38
38
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
39
39
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
40
40
 
41
- // ../open-contracts/dist/client/storage.js
41
+ // node_modules/.pnpm/@hasna+contracts@0.5.2/node_modules/@hasna/contracts/dist/client/storage.js
42
42
  var __defProp2 = Object.defineProperty;
43
43
  var __returnValue = (v) => v;
44
44
  function __exportSetter(name, newValue) {
@@ -6497,6 +6497,13 @@ class CloudShortlinksStore {
6497
6497
  const domains = await this.listDomains();
6498
6498
  return domains.find((d) => d.default_domain) ?? domains[0] ?? null;
6499
6499
  }
6500
+ async deleteDomain(hostnameOrId) {
6501
+ const domain = await this.getDomain(hostnameOrId);
6502
+ if (!domain)
6503
+ throw new Error("Domain not found.");
6504
+ await this.transport.del(`/domains/${enc(domain.hostname)}`);
6505
+ return domain;
6506
+ }
6500
6507
  async createLink(input) {
6501
6508
  return this.client.create("links", {
6502
6509
  url: input.destinationUrl,
@@ -6548,7 +6555,7 @@ class CloudShortlinksStore {
6548
6555
  const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
6549
6556
  if (!link)
6550
6557
  throw new Error("Link not found.");
6551
- await this.client.delete("links", link.slug, maybeSlug ? { query: { domain: domainOrSlug } } : {});
6558
+ await this.transport.del(`/links/${enc(link.slug)}`, undefined, maybeSlug ? { query: { domain: domainOrSlug } } : {});
6552
6559
  return link;
6553
6560
  }
6554
6561
  async recordClick(_link, _input = {}) {
@@ -6967,6 +6974,17 @@ class ShortlinksStore {
6967
6974
  `).get(normalized, hostnameOrId);
6968
6975
  return row ? domainFromRow(row) : null;
6969
6976
  }
6977
+ deleteDomain(hostnameOrId) {
6978
+ const domain = this.getDomain(hostnameOrId);
6979
+ if (!domain)
6980
+ throw new Error("Domain not found.");
6981
+ this.database.db.query("DELETE FROM domains WHERE id = ?").run(domain.id);
6982
+ const config = loadConfig();
6983
+ if (config.defaultDomain && normalizeHostname(config.defaultDomain) === domain.hostname) {
6984
+ updateConfig({ defaultDomain: undefined, publicBaseUrl: undefined });
6985
+ }
6986
+ return domain;
6987
+ }
6970
6988
  getDefaultDomain() {
6971
6989
  const config = loadConfig();
6972
6990
  if (config.defaultDomain) {
@@ -7170,6 +7188,9 @@ class LocalStore {
7170
7188
  async getDefaultDomain() {
7171
7189
  return this.inner.getDefaultDomain();
7172
7190
  }
7191
+ async deleteDomain(hostnameOrId) {
7192
+ return this.inner.deleteDomain(hostnameOrId);
7193
+ }
7173
7194
  async createLink(input) {
7174
7195
  return this.inner.createLink(input);
7175
7196
  }
@@ -7217,27 +7238,67 @@ async function withStore(fn, env = process.env, options = {}) {
7217
7238
  }
7218
7239
 
7219
7240
  // src/mcp/http.ts
7220
- import {
7221
- isHttpMode as harnessIsHttpMode,
7222
- isStdioMode as harnessIsStdioMode,
7223
- resolveMcpHttpPort as harnessResolveMcpHttpPort
7224
- } from "@hasna/mcp-harness";
7225
- import { startMcpHttpServer as harnessStartMcpHttpServer } from "@hasna/mcp-harness/node";
7241
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
7226
7242
  var MCP_HTTP_SERVICE_NAME = "shortlinks";
7243
+ var MCP_HTTP_HOST = "127.0.0.1";
7227
7244
  var DEFAULT_MCP_HTTP_PORT = 8851;
7228
7245
  function isHttpMode(argv = process.argv, env = process.env) {
7229
- return harnessIsHttpMode(argv, env);
7246
+ return argv.includes("--http") || env.MCP_HTTP === "1";
7230
7247
  }
7231
7248
  function resolveMcpHttpPort(argv = process.argv, env = process.env) {
7232
- return harnessResolveMcpHttpPort({ argv, env, default: DEFAULT_MCP_HTTP_PORT });
7249
+ const portIdx = argv.indexOf("--port");
7250
+ if (portIdx >= 0 && argv[portIdx + 1]) {
7251
+ const parsed = Number(argv[portIdx + 1]);
7252
+ if (Number.isFinite(parsed))
7253
+ return parsed;
7254
+ }
7255
+ if (env.MCP_HTTP_PORT) {
7256
+ const parsed = Number(env.MCP_HTTP_PORT);
7257
+ if (Number.isFinite(parsed))
7258
+ return parsed;
7259
+ }
7260
+ return DEFAULT_MCP_HTTP_PORT;
7261
+ }
7262
+ function healthPayload(name) {
7263
+ return { status: "ok", name };
7264
+ }
7265
+ async function handleMcpRequest(req, buildServer) {
7266
+ const transport = new WebStandardStreamableHTTPServerTransport({
7267
+ sessionIdGenerator: undefined
7268
+ });
7269
+ const server = buildServer();
7270
+ await server.connect(transport);
7271
+ return transport.handleRequest(req);
7233
7272
  }
7234
7273
  async function startMcpHttpServer(buildServer, options) {
7235
- return harnessStartMcpHttpServer(buildServer, {
7236
- port: options?.port,
7237
- host: options?.host,
7238
- serviceName: options?.serviceName ?? MCP_HTTP_SERVICE_NAME,
7239
- defaultPort: DEFAULT_MCP_HTTP_PORT
7274
+ const port = options?.port ?? DEFAULT_MCP_HTTP_PORT;
7275
+ const host = options?.host ?? MCP_HTTP_HOST;
7276
+ const serviceName = options?.serviceName ?? MCP_HTTP_SERVICE_NAME;
7277
+ const server = Bun.serve({
7278
+ hostname: host,
7279
+ port,
7280
+ async fetch(req) {
7281
+ const url = new URL(req.url);
7282
+ if (url.pathname === "/health" && req.method === "GET") {
7283
+ return Response.json(healthPayload(serviceName));
7284
+ }
7285
+ if (url.pathname === "/mcp") {
7286
+ return handleMcpRequest(req, buildServer);
7287
+ }
7288
+ return new Response("Not Found", { status: 404 });
7289
+ }
7240
7290
  });
7291
+ const boundPort = server.port ?? port;
7292
+ const resolvedUrl = `http://${host}:${boundPort}/mcp`;
7293
+ console.error(`${serviceName}-mcp HTTP listening on ${resolvedUrl}`);
7294
+ return {
7295
+ port: boundPort,
7296
+ host,
7297
+ url: resolvedUrl,
7298
+ async close() {
7299
+ await server.stop(true);
7300
+ }
7301
+ };
7241
7302
  }
7242
7303
 
7243
7304
  // src/mcp/index.ts
@@ -7344,6 +7405,15 @@ var TOOLS = [
7344
7405
  required: ["hostname"]
7345
7406
  }
7346
7407
  },
7408
+ {
7409
+ name: "delete_domain",
7410
+ description: "Delete a shortlink domain and all of its links and clicks.",
7411
+ inputSchema: {
7412
+ type: "object",
7413
+ properties: { hostname: { type: "string", description: "Hostname or domain id." } },
7414
+ required: ["hostname"]
7415
+ }
7416
+ },
7347
7417
  {
7348
7418
  name: "stats",
7349
7419
  description: "Total domains/links/clicks counts.",
@@ -7385,6 +7455,8 @@ async function dispatch(name, args) {
7385
7455
  originUrl: args.origin_url,
7386
7456
  notes: args.notes
7387
7457
  }));
7458
+ case "delete_domain":
7459
+ return withStore((s) => s.deleteDomain(args.hostname));
7388
7460
  case "stats":
7389
7461
  return withStore((s) => s.totalStats());
7390
7462
  default:
@@ -29,6 +29,7 @@ export declare class PgShortlinksStore {
29
29
  listDomains(): Promise<Domain[]>;
30
30
  getDomain(hostnameOrId: string): Promise<Domain | null>;
31
31
  getDefaultDomain(): Promise<Domain | null>;
32
+ deleteDomain(hostnameOrId: string): Promise<Domain>;
32
33
  createLink(input: CreateLinkInput): Promise<Link>;
33
34
  listLinks(options?: {
34
35
  domain?: string;
package/dist/pg-store.js CHANGED
@@ -474,6 +474,13 @@ class PgShortlinksStore {
474
474
  `);
475
475
  return row ? domainFromRow(row) : null;
476
476
  }
477
+ async deleteDomain(hostnameOrId) {
478
+ const domain = await this.getDomain(hostnameOrId);
479
+ if (!domain)
480
+ throw new Error("Domain not found.");
481
+ await this.pg.run("DELETE FROM domains WHERE id = ?", domain.id);
482
+ return domain;
483
+ }
477
484
  async createLink(input) {
478
485
  const domain = input.domain ? await this.getDomain(input.domain) : await this.getDefaultDomain();
479
486
  if (!domain) {
@@ -87,6 +87,10 @@ export interface DeleteResponse {
87
87
  "deleted": boolean;
88
88
  "slug"?: string;
89
89
  }
90
+ export interface DomainDeleteResponse {
91
+ "deleted": boolean;
92
+ "hostname"?: string;
93
+ }
90
94
  export interface HealthStatus {
91
95
  "status": string;
92
96
  "version": string;
@@ -139,6 +143,8 @@ export declare class ShortlinksApiClient {
139
143
  listDomains(init?: RequestInit): Promise<DomainList>;
140
144
  /** Add or update a domain. */
141
145
  addDomain(body: AddDomainRequest, init?: RequestInit): Promise<Domain>;
146
+ /** Delete a domain and all of its links and clicks. */
147
+ deleteDomain(hostname: string, init?: RequestInit): Promise<DomainDeleteResponse>;
142
148
  /** List shortlinks. */
143
149
  listLinks(query?: {
144
150
  "domain"?: string;
package/dist/sdk/index.js CHANGED
@@ -115,6 +115,13 @@ class ShortlinksApiClient {
115
115
  init
116
116
  });
117
117
  }
118
+ async deleteDomain(hostname, init) {
119
+ return this.request("DELETE", `/v1/domains/${encodeURIComponent(String(hostname))}`, {
120
+ body: undefined,
121
+ query: undefined,
122
+ init
123
+ });
124
+ }
118
125
  async listLinks(query, init) {
119
126
  return this.request("GET", `/v1/links`, {
120
127
  body: undefined,
@@ -311,6 +318,11 @@ function buildOpenApiDocument(version) {
311
318
  properties: { deleted: { type: "boolean" }, slug: { type: "string" } },
312
319
  required: ["deleted"]
313
320
  },
321
+ DomainDeleteResponse: {
322
+ type: "object",
323
+ properties: { deleted: { type: "boolean" }, hostname: { type: "string" } },
324
+ required: ["deleted"]
325
+ },
314
326
  HealthStatus: probe({ db_latency_ms: { type: "integer" } }),
315
327
  ReadyStatus: probe({ pending_migrations: { type: "array", items: { type: "string" } } }),
316
328
  VersionInfo: probe({ name: { type: "string" } }),
@@ -381,6 +393,19 @@ function buildOpenApiDocument(version) {
381
393
  }
382
394
  }
383
395
  },
396
+ "/v1/domains/{hostname}": {
397
+ delete: {
398
+ operationId: "deleteDomain",
399
+ summary: "Delete a domain and all of its links and clicks.",
400
+ security: [{ apiKey: [] }],
401
+ parameters: [
402
+ { name: "hostname", in: "path", required: true, schema: { type: "string" } }
403
+ ],
404
+ responses: {
405
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DomainDeleteResponse" } } } }
406
+ }
407
+ }
408
+ },
384
409
  "/v1/links": {
385
410
  get: {
386
411
  operationId: "listLinks",