terra-mcp-google 0.1.13 → 0.1.15

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 (44) hide show
  1. package/README.md +49 -0
  2. package/dist/admin/api.d.ts +35 -0
  3. package/dist/admin/api.js +106 -0
  4. package/dist/admin/api.js.map +1 -0
  5. package/dist/admin/server.d.ts +14 -0
  6. package/dist/admin/server.js +138 -0
  7. package/dist/admin/server.js.map +1 -0
  8. package/dist/admin/ui/assets/index--VIJVbeC.js +41 -0
  9. package/dist/admin/ui/assets/index-BykS1oj0.css +1 -0
  10. package/dist/admin/ui/index.html +23 -0
  11. package/dist/cli.js +15 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config/constants.d.ts +61 -3
  14. package/dist/config/constants.js +65 -13
  15. package/dist/config/constants.js.map +1 -1
  16. package/dist/core/local-file.js +4 -4
  17. package/dist/core/local-file.js.map +1 -1
  18. package/dist/core/tool.d.ts +5 -0
  19. package/dist/core/tool.js +11 -4
  20. package/dist/core/tool.js.map +1 -1
  21. package/dist/google/auth.js +2 -2
  22. package/dist/google/auth.js.map +1 -1
  23. package/dist/policy/engine.d.ts +29 -0
  24. package/dist/policy/engine.js +95 -0
  25. package/dist/policy/engine.js.map +1 -0
  26. package/dist/policy/guard.d.ts +31 -0
  27. package/dist/policy/guard.js +116 -0
  28. package/dist/policy/guard.js.map +1 -0
  29. package/dist/policy/resolver.d.ts +27 -0
  30. package/dist/policy/resolver.js +46 -0
  31. package/dist/policy/resolver.js.map +1 -0
  32. package/dist/policy/store.d.ts +32 -0
  33. package/dist/policy/store.js +135 -0
  34. package/dist/policy/store.js.map +1 -0
  35. package/dist/policy/types.d.ts +77 -0
  36. package/dist/policy/types.js +9 -0
  37. package/dist/policy/types.js.map +1 -0
  38. package/dist/services/drive/tools.js +45 -3
  39. package/dist/services/drive/tools.js.map +1 -1
  40. package/dist/services/sheets/tools.js +24 -0
  41. package/dist/services/sheets/tools.js.map +1 -1
  42. package/dist/setup/setup.js +2 -2
  43. package/dist/setup/setup.js.map +1 -1
  44. package/package.json +9 -2
package/README.md CHANGED
@@ -18,8 +18,57 @@ terra-mcp client codex # print safe (read-only) MCP config
18
18
  | `auth login` / `logout` / `status` | sign in / out / show account, scopes, expiry |
19
19
  | `setup` | check config dir + auth, print MCP config for all clients |
20
20
  | `client [codex\|claude\|copilot\|kiro\|all]` | print MCP config with **mutating tools disabled** (`--include-dangerous` keeps them) |
21
+ | `admin` | open the web console to manage which resources the agent may access (`-p` port, `--no-open`) |
21
22
  | *(no command)* | start the stdio server |
22
23
 
24
+ ## Configuration
25
+
26
+ Set these in your MCP client's `env` block (or the launching shell):
27
+
28
+ | Variable | Default | Purpose |
29
+ | --- | --- | --- |
30
+ | `TERRA_MCP_DIR` | `~/.terra-mcp` | Directory holding the OAuth client config + cached token |
31
+ | `GOOGLE_OAUTH_CREDENTIALS` | `<TERRA_MCP_DIR>/client_secret.json` | Google OAuth client JSON; overrides the embedded client |
32
+ | `GOOGLE_OAUTH_TOKEN` | `<TERRA_MCP_DIR>/token.json` | Cached access/refresh token |
33
+ | `TERRA_MCP_SAFE_MODE` | unset | `1` → register **only read-only tools**; drop every mutating tool |
34
+ | `TERRA_MCP_LOCAL_FILE_ROOT` | unset | Only directory `local_path`/`save_path` may touch. Unset = local file up/download disabled |
35
+ | `TERRA_MCP_POLICY_DB` | `<TERRA_MCP_DIR>/policy.db` | SQLite database holding the resource allowlist + policy mode |
36
+ | `TERRA_MCP_POLICY_MODE` | `read_open` | Initial mode before one is chosen in the console: `off` \| `read_open` \| `strict` |
37
+ | `TERRA_MCP_ADMIN_PORT` | `4717` | Port the `admin` web console listens on |
38
+ | `TERRA_MCP_TOKEN_PROXY_URL` | bundled proxy | OAuth token-exchange proxy (advanced; only when self-hosting it) |
39
+ | `TERRA_MCP_PROXY_KEY` | bundled key | Deterrent key sent to the proxy (ships in the package — not a secret) |
40
+
41
+ **Restricting tools.** Disable the mutating tools server-side with `TERRA_MCP_SAFE_MODE=1`, or
42
+ per-client with `terra-mcp client <agent>` (pass `--include-dangerous` to keep them on). Tools are
43
+ also gated by the **OAuth scopes granted at login** — a Sheets-only grant exposes no `drive_*` tools.
44
+
45
+ ## Permission gate (per-resource allowlist)
46
+
47
+ Beyond on/off tool gating, the server enforces a **per-resource allowlist**: you decide exactly
48
+ which Drive files/folders and Sheets the agent may touch, and with what power (read / write / delete).
49
+ Every tool call is checked against the allowlist (stored in a small local SQLite database) before it
50
+ runs; created resources are auto-added so the agent can keep working with what it just made.
51
+
52
+ ```bash
53
+ terra-mcp admin # opens the web console at http://localhost:4717
54
+ ```
55
+
56
+ **Three modes** (switch any time in the console):
57
+
58
+ | Mode | The agent can… |
59
+ | --- | --- |
60
+ | `off` | …do anything your account can (gate disabled). |
61
+ | `read_open` *(default)* | …**read** anything and **create** new resources, but only **write/delete existing** resources you've granted. Useful out of the box — nothing to configure to start reading. |
62
+ | `strict` | …only **see and use** resources in the allowlist (and inside granted folders). Everything else is invisible, even to list/search. |
63
+
64
+ **Grants** carry independent read / write / delete flags (new grants default to read-only — the safe
65
+ starting point), and a grant on a **folder cascades** to everything inside it. Manage them — search
66
+ your Drive to pick a file, or paste an ID — in the `admin` console. The console and the running
67
+ server share the same database, so changes take effect on the agent's next call; no restart needed.
68
+
69
+ > Requires **Node 23.4+** (or Node 22.5+ launched with `--experimental-sqlite`) for the gate. On
70
+ > older Node the server still runs, with the gate disabled.
71
+
23
72
  ## Tools
24
73
 
25
74
  **Auth** — sign-in/out are CLI-only (`terra-mcp auth login`/`logout`).
@@ -0,0 +1,35 @@
1
+ import type { drive_v3 } from "googleapis";
2
+ import type { PolicyStore } from "../policy/store.js";
3
+ import type { ResourceKind } from "../policy/types.js";
4
+ /** A Drive resource as surfaced to the admin console's "search & pick" flow. */
5
+ export interface DriveItem {
6
+ id: string;
7
+ name: string;
8
+ mimeType: string;
9
+ kind: ResourceKind;
10
+ }
11
+ /** Collaborators the API routes need, injected so the router stays testable. */
12
+ export interface ApiDeps {
13
+ store: PolicyStore;
14
+ searchDrive: (query: string) => Promise<DriveItem[]>;
15
+ authInfo: () => Promise<{
16
+ signedIn: boolean;
17
+ email: string | null;
18
+ }>;
19
+ }
20
+ /** A plain HTTP-ish response the transport layer serializes. */
21
+ export interface ApiResponse {
22
+ status: number;
23
+ body: unknown;
24
+ }
25
+ /** Classify a Drive mimeType into the grant `kind` the gate understands. */
26
+ export declare function kindOfMime(mimeType: string | null | undefined): ResourceKind;
27
+ /** Build the `searchDrive` dependency backed by a live Drive client. */
28
+ export declare function driveSearcher(drive: drive_v3.Drive): (query: string) => Promise<DriveItem[]>;
29
+ /**
30
+ * The admin console's HTTP surface, as a pure function over (method, path, body)
31
+ * — no `node:http` objects — so it unit-tests against an in-memory store and
32
+ * fake deps. Returns `null` for non-`/api` paths so the caller can fall back to
33
+ * serving the SPA's static assets.
34
+ */
35
+ export declare function routeApi(method: string, path: string, query: URLSearchParams, body: unknown, deps: ApiDeps): Promise<ApiResponse | null>;
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ import { DriveFileAdapter } from "../services/drive/adapter.js";
3
+ const kindSchema = z.enum(["file", "folder", "spreadsheet"]);
4
+ const modeSchema = z.enum(["off", "read_open", "strict"]);
5
+ const createGrantSchema = z.object({
6
+ kind: kindSchema,
7
+ googleId: z.string().min(1),
8
+ name: z.string().nullish(),
9
+ canRead: z.boolean(),
10
+ canWrite: z.boolean(),
11
+ canDelete: z.boolean(),
12
+ });
13
+ const patchGrantSchema = z.object({
14
+ name: z.string().nullish(),
15
+ canRead: z.boolean().optional(),
16
+ canWrite: z.boolean().optional(),
17
+ canDelete: z.boolean().optional(),
18
+ });
19
+ /** Classify a Drive mimeType into the grant `kind` the gate understands. */
20
+ export function kindOfMime(mimeType) {
21
+ if (mimeType === "application/vnd.google-apps.folder")
22
+ return "folder";
23
+ if (mimeType === "application/vnd.google-apps.spreadsheet")
24
+ return "spreadsheet";
25
+ return "file";
26
+ }
27
+ /** Build the `searchDrive` dependency backed by a live Drive client. */
28
+ export function driveSearcher(drive) {
29
+ return async (query) => {
30
+ const trimmed = query.trim();
31
+ // Escape single quotes for the Drive `q` string literal.
32
+ const q = trimmed ? `name contains '${trimmed.replace(/'/g, "\\'")}'` : undefined;
33
+ const { files } = await new DriveFileAdapter(drive).listFiles({
34
+ query: q,
35
+ pageSize: 25,
36
+ orderBy: "modifiedTime desc",
37
+ includeTrashed: false,
38
+ });
39
+ return files
40
+ .filter((f) => typeof f.id === "string")
41
+ .map((f) => ({
42
+ id: f.id,
43
+ name: f.name ?? "(untitled)",
44
+ mimeType: f.mimeType ?? "application/octet-stream",
45
+ kind: kindOfMime(f.mimeType),
46
+ }));
47
+ };
48
+ }
49
+ /**
50
+ * The admin console's HTTP surface, as a pure function over (method, path, body)
51
+ * — no `node:http` objects — so it unit-tests against an in-memory store and
52
+ * fake deps. Returns `null` for non-`/api` paths so the caller can fall back to
53
+ * serving the SPA's static assets.
54
+ */
55
+ export async function routeApi(method, path, query, body, deps) {
56
+ if (path !== "/api" && !path.startsWith("/api/"))
57
+ return null;
58
+ try {
59
+ if (path === "/api/health" && method === "GET") {
60
+ const auth = await deps.authInfo();
61
+ return ok({ ok: true, mode: deps.store.getMode(), ...auth });
62
+ }
63
+ if (path === "/api/grants") {
64
+ if (method === "GET")
65
+ return ok({ grants: deps.store.listGrants() });
66
+ if (method === "POST") {
67
+ const input = createGrantSchema.parse(body);
68
+ return ok({ grant: deps.store.upsertGrant(input) }, 201);
69
+ }
70
+ }
71
+ const grantId = path.match(/^\/api\/grants\/(\d+)$/);
72
+ if (grantId) {
73
+ const id = Number(grantId[1]);
74
+ if (method === "PATCH") {
75
+ const patch = patchGrantSchema.parse(body);
76
+ const grant = deps.store.updateGrant(id, patch);
77
+ return grant ? ok({ grant }) : fail(404, "Grant not found");
78
+ }
79
+ if (method === "DELETE") {
80
+ return deps.store.deleteGrant(id) ? ok({ ok: true }) : fail(404, "Grant not found");
81
+ }
82
+ }
83
+ if (path === "/api/mode" && method === "PUT") {
84
+ const { mode } = z.object({ mode: modeSchema }).parse(body);
85
+ deps.store.setMode(mode);
86
+ return ok({ mode });
87
+ }
88
+ if (path === "/api/drive/search" && method === "GET") {
89
+ return ok({ files: await deps.searchDrive(query.get("q") ?? "") });
90
+ }
91
+ return fail(404, "Not found");
92
+ }
93
+ catch (error) {
94
+ if (error instanceof z.ZodError) {
95
+ return fail(400, error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "));
96
+ }
97
+ return fail(500, error instanceof Error ? error.message : String(error));
98
+ }
99
+ }
100
+ function ok(body, status = 200) {
101
+ return { status, body };
102
+ }
103
+ function fail(status, error) {
104
+ return { status, body: { error } };
105
+ }
106
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export interface AdminServerHandle {
2
+ port: number;
3
+ url: string;
4
+ close: () => Promise<void>;
5
+ }
6
+ /**
7
+ * Start the admin web console: a tiny `node:http` server that exposes the policy
8
+ * REST API under `/api` and serves the built SPA for everything else. No
9
+ * framework, no extra runtime dependency.
10
+ */
11
+ export declare function startAdminServer(opts?: {
12
+ port?: number;
13
+ staticDir?: string;
14
+ }): Promise<AdminServerHandle>;
@@ -0,0 +1,138 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, extname, join, normalize, resolve } from "node:path";
5
+ import { ADMIN_PORT, getAdminStaticDir } from "../config/constants.js";
6
+ import { getGoogleClients } from "../google/client.js";
7
+ import { getAuthStatus } from "../google/auth.js";
8
+ import { getPolicyStoreOrThrow } from "../policy/guard.js";
9
+ import { driveSearcher, routeApi } from "./api.js";
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const MIME = {
12
+ ".html": "text/html; charset=utf-8",
13
+ ".js": "text/javascript; charset=utf-8",
14
+ ".css": "text/css; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8",
16
+ ".svg": "image/svg+xml",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".ico": "image/x-icon",
20
+ ".woff2": "font/woff2",
21
+ ".woff": "font/woff",
22
+ };
23
+ /** Build the API dependencies from the live policy store + Google clients. */
24
+ function buildDeps() {
25
+ return {
26
+ store: getPolicyStoreOrThrow(),
27
+ searchDrive: async (query) => driveSearcher((await getGoogleClients()).drive)(query),
28
+ authInfo: async () => {
29
+ const status = await getAuthStatus().catch(() => ({ authenticated: false, email: null }));
30
+ return { signedIn: status.authenticated, email: status.email ?? null };
31
+ },
32
+ };
33
+ }
34
+ /** First existing candidate directory holding the built SPA, or undefined. */
35
+ async function resolveStaticDir(override) {
36
+ const candidates = [
37
+ override,
38
+ getAdminStaticDir(),
39
+ join(HERE, "ui"), // production: copied beside the compiled server (dist/admin/ui)
40
+ join(HERE, "../../admin/dist"), // dev via tsx: the repo's built SPA
41
+ ].filter((c) => typeof c === "string");
42
+ const exists = await Promise.all(candidates.map(isDir));
43
+ return candidates.find((_, index) => exists[index]);
44
+ }
45
+ async function isDir(path) {
46
+ try {
47
+ return (await stat(path)).isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async function readBody(req) {
54
+ const chunks = [];
55
+ for await (const chunk of req)
56
+ chunks.push(chunk);
57
+ if (chunks.length === 0)
58
+ return undefined;
59
+ const raw = Buffer.concat(chunks).toString("utf8");
60
+ return raw ? JSON.parse(raw) : undefined;
61
+ }
62
+ function sendJson(res, status, body) {
63
+ const payload = JSON.stringify(body);
64
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
65
+ res.end(payload);
66
+ }
67
+ async function serveStatic(res, staticDir, urlPath) {
68
+ if (!staticDir) {
69
+ res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
70
+ res.end("Admin UI is not built. Run `pnpm admin:build` (or set TERRA_MCP_ADMIN_STATIC_DIR).");
71
+ return;
72
+ }
73
+ // Resolve the request to a file inside staticDir; fall back to index.html so
74
+ // the single-page app can handle its own routing. Reject path traversal.
75
+ const relative = normalize(urlPath === "/" ? "/index.html" : urlPath).replace(/^(\.\.[/\\])+/, "");
76
+ let filePath = resolve(staticDir, "." + relative);
77
+ if (!filePath.startsWith(resolve(staticDir)))
78
+ filePath = join(staticDir, "index.html");
79
+ if (!(await fileExists(filePath)))
80
+ filePath = join(staticDir, "index.html");
81
+ try {
82
+ const data = await readFile(filePath);
83
+ res.writeHead(200, { "content-type": MIME[extname(filePath)] ?? "application/octet-stream" });
84
+ res.end(data);
85
+ }
86
+ catch {
87
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
88
+ res.end("Not found");
89
+ }
90
+ }
91
+ async function fileExists(path) {
92
+ try {
93
+ return (await stat(path)).isFile();
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Start the admin web console: a tiny `node:http` server that exposes the policy
101
+ * REST API under `/api` and serves the built SPA for everything else. No
102
+ * framework, no extra runtime dependency.
103
+ */
104
+ export async function startAdminServer(opts = {}) {
105
+ const port = opts.port ?? ADMIN_PORT;
106
+ const deps = buildDeps();
107
+ const staticDir = await resolveStaticDir(opts.staticDir);
108
+ return new Promise((resolvePromise, rejectPromise) => {
109
+ const server = createServer((req, res) => {
110
+ void handle(req, res, deps, staticDir);
111
+ });
112
+ server.on("error", rejectPromise);
113
+ server.listen(port, () => {
114
+ resolvePromise({
115
+ port,
116
+ url: `http://localhost:${port}`,
117
+ close: () => new Promise((done) => server.close(() => done())),
118
+ });
119
+ });
120
+ });
121
+ }
122
+ async function handle(req, res, deps, staticDir) {
123
+ const url = new URL(req.url ?? "/", "http://localhost");
124
+ const method = req.method ?? "GET";
125
+ try {
126
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
127
+ const body = method === "GET" || method === "DELETE" ? undefined : await readBody(req);
128
+ const result = await routeApi(method, url.pathname, url.searchParams, body, deps);
129
+ if (result)
130
+ return sendJson(res, result.status, result.body);
131
+ }
132
+ await serveStatic(res, staticDir, url.pathname);
133
+ }
134
+ catch (error) {
135
+ sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
136
+ }
137
+ }
138
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"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"}