@miadi/connector 0.0.0-stage → 0.1.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.
@@ -0,0 +1,18 @@
1
+ import { MiadiOAuthProvider } from "./oauth.js";
2
+ import type { MiadiPerson } from "./people.js";
3
+ import { Ledger } from "./store.js";
4
+ import type { ConnectorRecord } from "./records.js";
5
+ import type { ConnectorConfig } from "./config.js";
6
+ export interface ConnectorDeps {
7
+ identify: (token: string) => Promise<MiadiPerson | null>;
8
+ fetchImpl?: typeof fetch;
9
+ /** The factory directory holding queue/ (default ~/.miadi/factory). */
10
+ factoryDir?: string;
11
+ tmuxSessions?: () => string[];
12
+ }
13
+ export declare function createConnectorApp(config: ConnectorConfig, deps: ConnectorDeps): {
14
+ app: import("express-serve-static-core").Express;
15
+ provider: MiadiOAuthProvider;
16
+ ledger: Ledger<ConnectorRecord>;
17
+ };
18
+ export declare const INSTRUCTIONS = "Miadi is a relational software factory: agent sessions, Miadi reviews of screenwalks, Chronicle episodes, talking circles and the five accountable circles (Story, Stewardship, Engineering, Evaluation, Continuity) across three domains (Creative, Ceremonial, Engineering).\n\nStart with get_capabilities: it says who you are signed in as, which tools are live, and which return not_implemented.\n\nRules every tool keeps:\n- Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.\n- Requests (runs, review operations, Deep Diver, review-to-episode links, session completion, calendar events) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person approves it in Miadi.\n- An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.\n- Use the miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n and session:<uuid> reference forms.";
package/dist/server.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The connector's HTTP surface:
3
+ *
4
+ * /.well-known/oauth-* OAuth discovery (MCP authorization spec)
5
+ * /authorize /token /register /revoke, /authorize/confirm sign-in
6
+ * /mcp the MCP endpoint (streamable HTTP, stateless)
7
+ * /health liveness, no data
8
+ *
9
+ * Every MCP request carries the signed-in person, and each tool call runs
10
+ * as that person (see tools.ts).
11
+ */
12
+ import express from "express";
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
15
+ import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
16
+ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
17
+ import { MiadiOAuthProvider, SCOPES, emptyOAuthState } from "./oauth.js";
18
+ import { registerApprovalRoutes } from "./approve.js";
19
+ import { JsonFile, Ledger, ensureStateDir, statePaths } from "./store.js";
20
+ import { registerTools } from "./tools.js";
21
+ import { MiadiClient } from "./miadi.js";
22
+ import { ReviewClient } from "./reviews.js";
23
+ import { VERSION } from "./version.js";
24
+ export function createConnectorApp(config, deps) {
25
+ ensureStateDir(config.stateDir);
26
+ const paths = statePaths(config.stateDir);
27
+ const oauthState = new JsonFile(paths.oauth, emptyOAuthState);
28
+ const ledger = new Ledger(paths.ledger);
29
+ const provider = new MiadiOAuthProvider(oauthState, deps.identify);
30
+ const miadi = new MiadiClient(config.miadiUrl, deps.fetchImpl);
31
+ const reviews = new ReviewClient(config.reviewUrl, config.reviewToken, deps.fetchImpl);
32
+ const publicUrl = new URL(config.publicUrl);
33
+ const mcpUrl = new URL("/mcp", publicUrl);
34
+ const app = express();
35
+ app.disable("x-powered-by");
36
+ // cloudflared reaches the connector over loopback; only that hop is trusted for the client address.
37
+ app.set("trust proxy", "loopback");
38
+ app.get("/health", (_req, res) => {
39
+ res.json({ ok: true, service: "@miadi/connector", version: VERSION });
40
+ });
41
+ app.use(mcpAuthRouter({
42
+ provider,
43
+ issuerUrl: publicUrl,
44
+ baseUrl: publicUrl,
45
+ resourceServerUrl: mcpUrl,
46
+ scopesSupported: SCOPES,
47
+ resourceName: "Miadi",
48
+ serviceDocumentationUrl: new URL("https://github.com/jgwill/Miadi/tree/main/packages/connector"),
49
+ }));
50
+ app.post("/authorize/confirm", express.urlencoded({ extended: false, limit: "16kb" }), (req, res, next) => {
51
+ provider.confirm(req, res).catch(next);
52
+ });
53
+ registerApprovalRoutes(app, {
54
+ config,
55
+ ledger,
56
+ miadi,
57
+ identify: deps.identify,
58
+ fetchImpl: deps.fetchImpl,
59
+ factoryDir: deps.factoryDir,
60
+ tmuxSessions: deps.tmuxSessions,
61
+ });
62
+ const auth = requireBearerAuth({
63
+ verifier: provider,
64
+ requiredScopes: ["miadi:read"],
65
+ resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpUrl),
66
+ });
67
+ const handle = async (req, res) => {
68
+ const ctx = { config, miadi, reviews, ledger, oauthState, factoryDir: deps.factoryDir };
69
+ const server = new McpServer({ name: "miadi", version: VERSION }, { instructions: INSTRUCTIONS });
70
+ registerTools(server, ctx);
71
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
72
+ res.on("close", () => {
73
+ transport.close().catch(() => { });
74
+ server.close().catch(() => { });
75
+ });
76
+ try {
77
+ await server.connect(transport);
78
+ await transport.handleRequest(req, res, req.body);
79
+ }
80
+ catch (error) {
81
+ if (!res.headersSent) {
82
+ res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null });
83
+ }
84
+ console.error("[connector] /mcp", error);
85
+ }
86
+ };
87
+ app.post("/mcp", auth, express.json({ limit: "4mb" }), handle);
88
+ app.get("/mcp", auth, (_req, res) => {
89
+ res.status(405).set("Allow", "POST").json({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed" }, id: null });
90
+ });
91
+ app.delete("/mcp", auth, (_req, res) => {
92
+ res.status(405).set("Allow", "POST").json({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed" }, id: null });
93
+ });
94
+ return { app, provider, ledger };
95
+ }
96
+ export const INSTRUCTIONS = `Miadi is a relational software factory: agent sessions, Miadi reviews of screenwalks, Chronicle episodes, talking circles and the five accountable circles (Story, Stewardship, Engineering, Evaluation, Continuity) across three domains (Creative, Ceremonial, Engineering).
97
+
98
+ Start with get_capabilities: it says who you are signed in as, which tools are live, and which return not_implemented.
99
+
100
+ Rules every tool keeps:
101
+ - Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.
102
+ - Requests (runs, review operations, Deep Diver, review-to-episode links, session completion, calendar events) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person approves it in Miadi.
103
+ - An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.
104
+ - Use the miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n and session:<uuid> reference forms.`;
@@ -0,0 +1,23 @@
1
+ export declare function digest(secret: string): string;
2
+ export declare function newSecret(bytes?: number): string;
3
+ export declare function ensureStateDir(dir: string): void;
4
+ export declare class JsonFile<T> {
5
+ private readonly path;
6
+ private readonly empty;
7
+ constructor(path: string, empty: () => T);
8
+ read(): T;
9
+ write(value: T): void;
10
+ update(fn: (value: T) => void): T;
11
+ }
12
+ export declare class Ledger<R extends {
13
+ id: string;
14
+ }> {
15
+ private readonly path;
16
+ constructor(path: string);
17
+ append(record: R): R;
18
+ all(): R[];
19
+ }
20
+ export declare function statePaths(dir: string): {
21
+ oauth: string;
22
+ ledger: string;
23
+ };
package/dist/store.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The connector's own state, kept in one directory (default
3
+ * /srv/miadi/connector, which the daily /srv/miadi backup carries).
4
+ *
5
+ * - oauth.json registered OAuth clients, pending sign-ins, codes and tokens.
6
+ * Tokens are stored as sha256 digests, never as issued. The
7
+ * person's Miadi token is kept so the connector can act as
8
+ * them; the directory is created 0700 for that reason.
9
+ * - ledger.jsonl the records people contribute through the connector
10
+ * (observations, handoffs, proposals). Append-only: a record
11
+ * is never edited, a later record supersedes it.
12
+ */
13
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
14
+ import { createHash, randomBytes } from "node:crypto";
15
+ import { join } from "node:path";
16
+ import { readJsonSafe, writeJsonAtomic } from "@miadi/node-service-kit/json";
17
+ export function digest(secret) {
18
+ return createHash("sha256").update(secret).digest("hex");
19
+ }
20
+ export function newSecret(bytes = 32) {
21
+ return randomBytes(bytes).toString("base64url");
22
+ }
23
+ export function ensureStateDir(dir) {
24
+ if (!existsSync(dir))
25
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
26
+ try {
27
+ chmodSync(dir, 0o700);
28
+ }
29
+ catch {
30
+ /* a directory owned by another user keeps its mode; reads still work */
31
+ }
32
+ }
33
+ export class JsonFile {
34
+ path;
35
+ empty;
36
+ constructor(path, empty) {
37
+ this.path = path;
38
+ this.empty = empty;
39
+ }
40
+ read() {
41
+ const r = readJsonSafe(this.path);
42
+ if (r.ok)
43
+ return r.data;
44
+ if (r.missing)
45
+ return this.empty();
46
+ throw new Error(`${this.path} could not be read (${r.corrupt ? "corrupt" : r.code})`);
47
+ }
48
+ write(value) {
49
+ writeJsonAtomic(this.path, value);
50
+ }
51
+ update(fn) {
52
+ const value = this.read();
53
+ fn(value);
54
+ this.write(value);
55
+ return value;
56
+ }
57
+ }
58
+ export class Ledger {
59
+ path;
60
+ constructor(path) {
61
+ this.path = path;
62
+ }
63
+ append(record) {
64
+ appendFileSync(this.path, JSON.stringify(record) + "\n", { mode: 0o600 });
65
+ return record;
66
+ }
67
+ all() {
68
+ if (!existsSync(this.path))
69
+ return [];
70
+ const out = [];
71
+ for (const line of readFileSync(this.path, "utf8").split("\n")) {
72
+ if (!line.trim())
73
+ continue;
74
+ try {
75
+ out.push(JSON.parse(line));
76
+ }
77
+ catch {
78
+ /* a torn last line from a crash is skipped, not fatal */
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ }
84
+ export function statePaths(dir) {
85
+ return {
86
+ oauth: join(dir, "oauth.json"),
87
+ ledger: join(dir, "ledger.jsonl"),
88
+ };
89
+ }
@@ -0,0 +1,19 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ConnectorConfig } from "./config.js";
3
+ import type { MiadiClient } from "./miadi.js";
4
+ import type { ReviewClient } from "./reviews.js";
5
+ import type { JsonFile, Ledger } from "./store.js";
6
+ import type { OAuthState } from "./oauth.js";
7
+ import { type ConnectorRecord } from "./records.js";
8
+ export interface ToolContext {
9
+ config: ConnectorConfig;
10
+ miadi: MiadiClient;
11
+ reviews: ReviewClient;
12
+ ledger: Ledger<ConnectorRecord>;
13
+ oauthState: JsonFile<OAuthState>;
14
+ /** The factory directory holding queue/ (default ~/.miadi/factory). */
15
+ factoryDir?: string;
16
+ }
17
+ export declare function registerTools(server: McpServer, ctx: ToolContext): void;
18
+ /** What get_capabilities reports for each tool. */
19
+ export declare const TOOL_STATUS: Record<string, string>;