@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.
- package/README.md +6 -0
- package/dist/cli/index.js +283 -42
- package/dist/client-store.d.ts +1 -0
- package/dist/cloud-store.d.ts +1 -0
- package/dist/index.js +61 -3
- package/dist/mcp/http.d.ts +17 -12
- package/dist/mcp/index.js +87 -15
- package/dist/pg-store.d.ts +1 -0
- package/dist/pg-store.js +7 -0
- package/dist/sdk/generated.d.ts +6 -0
- package/dist/sdk/index.js +25 -0
- package/dist/serve/index.js +237 -165
- package/dist/server.js +11 -0
- package/dist/store-interface.d.ts +5 -0
- package/dist/store.d.ts +1 -0
- package/package.json +3 -4
package/dist/mcp/http.d.ts
CHANGED
|
@@ -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 —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
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
|
|
11
|
-
*
|
|
12
|
-
* `McpServer
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
7246
|
+
return argv.includes("--http") || env.MCP_HTTP === "1";
|
|
7230
7247
|
}
|
|
7231
7248
|
function resolveMcpHttpPort(argv = process.argv, env = process.env) {
|
|
7232
|
-
|
|
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
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
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:
|
package/dist/pg-store.d.ts
CHANGED
|
@@ -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) {
|
package/dist/sdk/generated.d.ts
CHANGED
|
@@ -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",
|