@schoolai/shipyard-mcp 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,20 @@
1
+ # Server Environment Variables
2
+ # Copy this file to .env and adjust values for local development
3
+
4
+ # Server configuration
5
+ NODE_ENV=development
6
+ LOG_LEVEL=info
7
+
8
+ # Registry configuration
9
+ REGISTRY_PORT=32191
10
+ SHIPYARD_STATE_DIR=~/.shipyard
11
+
12
+ # Web UI URL (for links generated by server)
13
+ SHIPYARD_WEB_URL=http://localhost:5173
14
+
15
+ # GitHub integration (optional - falls back to gh CLI)
16
+ GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
17
+
18
+ # GitHub artifacts feature (optional - defaults to enabled)
19
+ # Set to 'disabled', 'false', or '0' to disable
20
+ SHIPYARD_ARTIFACTS=true
@@ -0,0 +1,158 @@
1
+ import {
2
+ loadEnv,
3
+ logger
4
+ } from "./chunk-GSGLHRWX.js";
5
+
6
+ // src/server-identity.ts
7
+ import { execSync as execSync2 } from "child_process";
8
+
9
+ // src/config/env/github.ts
10
+ import { execSync } from "child_process";
11
+ import { z } from "zod";
12
+ function getTokenFromGhCli() {
13
+ try {
14
+ const token = execSync("gh auth token", {
15
+ encoding: "utf-8",
16
+ timeout: 5e3,
17
+ stdio: ["pipe", "pipe", "pipe"]
18
+ // Suppress stderr
19
+ }).trim();
20
+ if (token) {
21
+ return token;
22
+ }
23
+ } catch {
24
+ }
25
+ return null;
26
+ }
27
+ var schema = z.object({
28
+ GITHUB_USERNAME: z.string().optional(),
29
+ GITHUB_TOKEN: z.string().optional().transform((val) => val || getTokenFromGhCli() || null),
30
+ SHIPYARD_ARTIFACTS: z.string().optional().transform((val) => {
31
+ if (!val) return true;
32
+ const setting = val.toLowerCase();
33
+ return setting !== "disabled" && setting !== "false" && setting !== "0";
34
+ })
35
+ });
36
+ var githubConfig = loadEnv(schema);
37
+
38
+ // src/server-identity.ts
39
+ var cachedUsername = null;
40
+ var usernameResolved = false;
41
+ var cachedRepoName = null;
42
+ function getRepositoryFullName() {
43
+ if (cachedRepoName !== null) {
44
+ return cachedRepoName || null;
45
+ }
46
+ try {
47
+ const repoName = execSync2("gh repo view --json nameWithOwner --jq .nameWithOwner", {
48
+ encoding: "utf-8",
49
+ timeout: 5e3,
50
+ stdio: ["pipe", "pipe", "pipe"]
51
+ }).trim();
52
+ if (!repoName) {
53
+ cachedRepoName = "";
54
+ return null;
55
+ }
56
+ cachedRepoName = repoName;
57
+ return cachedRepoName;
58
+ } catch {
59
+ cachedRepoName = "";
60
+ return null;
61
+ }
62
+ }
63
+ async function getGitHubUsername() {
64
+ if (usernameResolved && cachedUsername) {
65
+ return cachedUsername;
66
+ }
67
+ if (githubConfig.GITHUB_USERNAME) {
68
+ cachedUsername = githubConfig.GITHUB_USERNAME;
69
+ usernameResolved = true;
70
+ logger.info({ username: cachedUsername }, "Using GITHUB_USERNAME from env");
71
+ return cachedUsername;
72
+ }
73
+ if (githubConfig.GITHUB_TOKEN) {
74
+ const username = await getUsernameFromToken(githubConfig.GITHUB_TOKEN);
75
+ if (username) {
76
+ cachedUsername = username;
77
+ usernameResolved = true;
78
+ logger.info({ username }, "Resolved username from GITHUB_TOKEN via API");
79
+ return cachedUsername;
80
+ }
81
+ }
82
+ const cliUsername = getUsernameFromCLI();
83
+ if (cliUsername) {
84
+ cachedUsername = cliUsername;
85
+ usernameResolved = true;
86
+ logger.info({ username: cliUsername }, "Resolved username from gh CLI");
87
+ return cachedUsername;
88
+ }
89
+ const gitUsername = getUsernameFromGitConfig();
90
+ if (gitUsername) {
91
+ cachedUsername = gitUsername;
92
+ usernameResolved = true;
93
+ logger.warn({ username: gitUsername }, "Using git config user.name (UNVERIFIED)");
94
+ return cachedUsername;
95
+ }
96
+ const osUsername = process.env.USER || process.env.USERNAME;
97
+ if (osUsername) {
98
+ cachedUsername = osUsername.replace(/[^a-zA-Z0-9_-]/g, "_");
99
+ usernameResolved = true;
100
+ logger.warn(
101
+ { username: cachedUsername, original: osUsername },
102
+ "Using sanitized OS username (UNVERIFIED)"
103
+ );
104
+ return cachedUsername;
105
+ }
106
+ usernameResolved = true;
107
+ throw new Error(
108
+ 'GitHub username required but could not be determined.\n\nConfigure ONE of:\n1. GITHUB_USERNAME=your-username (explicit)\n2. GITHUB_TOKEN=ghp_xxx (will fetch from API)\n3. gh auth login (uses CLI)\n4. git config --global user.name "your-username"\n5. Set USER or USERNAME environment variable\n\nFor remote agents: Use option 1 or 2'
109
+ );
110
+ }
111
+ async function getUsernameFromToken(token) {
112
+ try {
113
+ const response = await fetch("https://api.github.com/user", {
114
+ headers: {
115
+ Authorization: `Bearer ${token}`,
116
+ Accept: "application/vnd.github.v3+json",
117
+ "User-Agent": "shipyard-mcp-server"
118
+ },
119
+ signal: AbortSignal.timeout(5e3)
120
+ });
121
+ if (!response.ok) return null;
122
+ const user = await response.json();
123
+ return user.login || null;
124
+ } catch (error) {
125
+ logger.debug({ error }, "GitHub API failed");
126
+ return null;
127
+ }
128
+ }
129
+ function getUsernameFromCLI() {
130
+ try {
131
+ const username = execSync2("gh api user --jq .login", {
132
+ encoding: "utf-8",
133
+ timeout: 5e3,
134
+ stdio: ["pipe", "pipe", "pipe"]
135
+ }).trim();
136
+ return username || null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ function getUsernameFromGitConfig() {
142
+ try {
143
+ const username = execSync2("git config user.name", {
144
+ encoding: "utf-8",
145
+ timeout: 5e3,
146
+ stdio: ["pipe", "pipe", "pipe"]
147
+ }).trim();
148
+ return username || null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ export {
155
+ githubConfig,
156
+ getRepositoryFullName,
157
+ getGitHubUsername
158
+ };
@@ -0,0 +1,120 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-GSGLHRWX.js";
4
+
5
+ // src/session-registry.ts
6
+ function isSessionStateCreated(state) {
7
+ return state.lifecycle === "created";
8
+ }
9
+ function isSessionStateSynced(state) {
10
+ return state.lifecycle === "synced";
11
+ }
12
+ function isSessionStateApprovedAwaitingToken(state) {
13
+ return state.lifecycle === "approved_awaiting_token";
14
+ }
15
+ function isSessionStateApproved(state) {
16
+ return state.lifecycle === "approved";
17
+ }
18
+ function isSessionStateReviewed(state) {
19
+ return state.lifecycle === "reviewed";
20
+ }
21
+ function assertNever(value) {
22
+ throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
23
+ }
24
+ var sessions = /* @__PURE__ */ new Map();
25
+ var planToSession = /* @__PURE__ */ new Map();
26
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
27
+ function getSessionState(sessionId) {
28
+ return sessions.get(sessionId) ?? null;
29
+ }
30
+ function touchSession(sessionId) {
31
+ const session = sessions.get(sessionId);
32
+ if (session) {
33
+ session.lastSyncedAt = Date.now();
34
+ }
35
+ }
36
+ function setSessionState(sessionId, state) {
37
+ const updatedState = {
38
+ ...state,
39
+ lastSyncedAt: Date.now()
40
+ };
41
+ sessions.set(sessionId, updatedState);
42
+ planToSession.set(updatedState.planId, sessionId);
43
+ }
44
+ function deleteSessionState(sessionId) {
45
+ const session = sessions.get(sessionId);
46
+ if (session) {
47
+ planToSession.delete(session.planId);
48
+ }
49
+ sessions.delete(sessionId);
50
+ }
51
+ function getSessionStateByPlanId(planId) {
52
+ const sessionId = planToSession.get(planId);
53
+ if (!sessionId) return null;
54
+ return getSessionState(sessionId);
55
+ }
56
+ function getSessionIdByPlanId(planId) {
57
+ return planToSession.get(planId) || null;
58
+ }
59
+ function cleanStaleSessions(ttlMs = DEFAULT_TTL_MS) {
60
+ const now = Date.now();
61
+ let cleaned = 0;
62
+ for (const [sessionId, session] of sessions.entries()) {
63
+ if (now - session.lastSyncedAt > ttlMs) {
64
+ const currentSessionId = planToSession.get(session.planId);
65
+ if (currentSessionId === sessionId) {
66
+ planToSession.delete(session.planId);
67
+ }
68
+ sessions.delete(sessionId);
69
+ cleaned++;
70
+ }
71
+ }
72
+ if (cleaned > 0) {
73
+ logger.info({ cleaned, ttlMs }, "Cleaned stale sessions from registry");
74
+ }
75
+ return cleaned;
76
+ }
77
+ function getActiveSessions() {
78
+ return Array.from(sessions.keys());
79
+ }
80
+ function getSessionCount() {
81
+ return sessions.size;
82
+ }
83
+ var cleanupInterval = null;
84
+ function startPeriodicCleanup(intervalMs = 15 * 60 * 1e3) {
85
+ if (cleanupInterval) {
86
+ logger.warn("Periodic cleanup already started");
87
+ return;
88
+ }
89
+ cleanupInterval = setInterval(() => {
90
+ cleanStaleSessions();
91
+ }, intervalMs);
92
+ logger.info({ intervalMs }, "Started periodic session cleanup");
93
+ }
94
+ function stopPeriodicCleanup() {
95
+ if (cleanupInterval) {
96
+ clearInterval(cleanupInterval);
97
+ cleanupInterval = null;
98
+ logger.info("Stopped periodic session cleanup");
99
+ }
100
+ }
101
+
102
+ export {
103
+ isSessionStateCreated,
104
+ isSessionStateSynced,
105
+ isSessionStateApprovedAwaitingToken,
106
+ isSessionStateApproved,
107
+ isSessionStateReviewed,
108
+ assertNever,
109
+ getSessionState,
110
+ touchSession,
111
+ setSessionState,
112
+ deleteSessionState,
113
+ getSessionStateByPlanId,
114
+ getSessionIdByPlanId,
115
+ cleanStaleSessions,
116
+ getActiveSessions,
117
+ getSessionCount,
118
+ startPeriodicCleanup,
119
+ stopPeriodicCleanup
120
+ };
@@ -0,0 +1,62 @@
1
+ // src/logger.ts
2
+ import { mkdirSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, join } from "path";
5
+ import pino from "pino";
6
+
7
+ // src/config/env/server.ts
8
+ import { z as z2 } from "zod";
9
+
10
+ // src/config/config.ts
11
+ import { z } from "zod";
12
+ function loadEnv(schema2) {
13
+ try {
14
+ return schema2.parse(process.env);
15
+ } catch (error) {
16
+ if (error instanceof z.ZodError) {
17
+ const testResult = schema2.safeParse(void 0);
18
+ if (testResult.success) {
19
+ return testResult.data;
20
+ }
21
+ if (!error.issues || !Array.isArray(error.issues)) {
22
+ throw new Error("Environment variable validation failed (no error details available)");
23
+ }
24
+ const errorMessages = error.issues.map((err) => ` - ${err.path.join(".")}: ${err.message}`).join("\n");
25
+ throw new Error(`Environment variable validation failed:
26
+ ${errorMessages}`);
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ // src/config/env/server.ts
33
+ var schema = z2.object({
34
+ NODE_ENV: z2.enum(["development", "test", "production"]).default("development"),
35
+ LOG_LEVEL: z2.enum(["debug", "info", "warn", "error"]).default("info")
36
+ });
37
+ var serverConfig = loadEnv(schema);
38
+
39
+ // src/logger.ts
40
+ var LOG_FILE = join(homedir(), ".shipyard", "server-debug.log");
41
+ try {
42
+ mkdirSync(dirname(LOG_FILE), { recursive: true });
43
+ } catch {
44
+ }
45
+ var streams = [
46
+ { stream: pino.destination(2) },
47
+ // stderr - CRITICAL: MCP uses stdout for protocol
48
+ { stream: pino.destination(LOG_FILE) }
49
+ // file for debugging
50
+ ];
51
+ var logger = pino(
52
+ {
53
+ level: serverConfig.LOG_LEVEL,
54
+ timestamp: pino.stdTimeFunctions.isoTime
55
+ },
56
+ pino.multistream(streams)
57
+ );
58
+
59
+ export {
60
+ loadEnv,
61
+ logger
62
+ };
@@ -0,0 +1,30 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
26
+
27
+ export {
28
+ __commonJS,
29
+ __toESM
30
+ };