@thenavidm/apple-photos-mcp-cli 1.0.0 → 1.0.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/lib/bridge.d.ts +43 -0
- package/lib/bridge.js +91 -0
- package/lib/bridge.js.map +1 -0
- package/lib/cli.d.ts +92 -0
- package/lib/cli.js +634 -0
- package/lib/cli.js.map +1 -0
- package/lib/config.d.ts +19 -0
- package/lib/config.js +58 -0
- package/lib/config.js.map +1 -0
- package/lib/errors.d.ts +15 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +117 -0
- package/lib/index.js.map +1 -0
- package/lib/safety.d.ts +27 -0
- package/lib/safety.js +70 -0
- package/lib/safety.js.map +1 -0
- package/lib/server.d.ts +10 -0
- package/lib/server.js +38 -0
- package/lib/server.js.map +1 -0
- package/lib/tools/index.d.ts +55 -0
- package/lib/tools/index.js +154 -0
- package/lib/tools/index.js.map +1 -0
- package/lib/tools/kit.d.ts +48 -0
- package/lib/tools/kit.js +64 -0
- package/lib/tools/kit.js.map +1 -0
- package/package.json +35 -10
- package/src/apple_photos_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/__main__.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/config.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/doctor.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/library.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/previews.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/safety.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/search.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/server.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/writes.cpython-313.pyc +0 -0
package/lib/bridge.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge to the Python engine.
|
|
3
|
+
*
|
|
4
|
+
* Apple Photos is the one case the house standard carves out for Python: the
|
|
5
|
+
* only libraries that can read a Photos library are `osxphotos` and
|
|
6
|
+
* `photoscript`, both Python-only, both macOS-only, both pulling in pyobjc.
|
|
7
|
+
* Reimplementing them in TypeScript would mean reimplementing Apple's private
|
|
8
|
+
* SQLite schema, which is not a thing to own.
|
|
9
|
+
*
|
|
10
|
+
* So the engine stays Python and this layer wraps it, the way
|
|
11
|
+
* google-workspace-mcp wraps the `gws` binary. What TypeScript buys is the part
|
|
12
|
+
* Python cost us: `npx` with no toolchain to install first, a CLI generated
|
|
13
|
+
* from the same tool array as the MCP server, and a static `ALL_TOOLS` that the
|
|
14
|
+
* HQ connector can import so the hosted surface cannot drift from this one.
|
|
15
|
+
*
|
|
16
|
+
* Calls are proxied to the Python MCP server over stdio using the official
|
|
17
|
+
* client, rather than a bespoke JSON protocol. That means the Python needs no
|
|
18
|
+
* changes at all, and there is exactly one implementation of every tool.
|
|
19
|
+
*/
|
|
20
|
+
import type { Config } from "./config.js";
|
|
21
|
+
export type BridgeResult = {
|
|
22
|
+
text: string;
|
|
23
|
+
isError: boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* One long-lived connection to the Python server.
|
|
27
|
+
*
|
|
28
|
+
* Spawning per call would pay `uv`'s dependency resolution and a Photos library
|
|
29
|
+
* scan every time, which is seconds each. The process is started on first use
|
|
30
|
+
* and reused.
|
|
31
|
+
*/
|
|
32
|
+
export declare class PythonBridge {
|
|
33
|
+
private readonly config;
|
|
34
|
+
private client?;
|
|
35
|
+
private starting?;
|
|
36
|
+
constructor(config: Config);
|
|
37
|
+
private connect;
|
|
38
|
+
/** Call one Python tool and return its text payload. */
|
|
39
|
+
call(tool: string, args: Record<string, unknown>): Promise<BridgeResult>;
|
|
40
|
+
/** What the Python server says it offers. Used by `doctor` to prove the two agree. */
|
|
41
|
+
listTools(): Promise<string[]>;
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
}
|
package/lib/bridge.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge to the Python engine.
|
|
3
|
+
*
|
|
4
|
+
* Apple Photos is the one case the house standard carves out for Python: the
|
|
5
|
+
* only libraries that can read a Photos library are `osxphotos` and
|
|
6
|
+
* `photoscript`, both Python-only, both macOS-only, both pulling in pyobjc.
|
|
7
|
+
* Reimplementing them in TypeScript would mean reimplementing Apple's private
|
|
8
|
+
* SQLite schema, which is not a thing to own.
|
|
9
|
+
*
|
|
10
|
+
* So the engine stays Python and this layer wraps it, the way
|
|
11
|
+
* google-workspace-mcp wraps the `gws` binary. What TypeScript buys is the part
|
|
12
|
+
* Python cost us: `npx` with no toolchain to install first, a CLI generated
|
|
13
|
+
* from the same tool array as the MCP server, and a static `ALL_TOOLS` that the
|
|
14
|
+
* HQ connector can import so the hosted surface cannot drift from this one.
|
|
15
|
+
*
|
|
16
|
+
* Calls are proxied to the Python MCP server over stdio using the official
|
|
17
|
+
* client, rather than a bespoke JSON protocol. That means the Python needs no
|
|
18
|
+
* changes at all, and there is exactly one implementation of every tool.
|
|
19
|
+
*/
|
|
20
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
21
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
22
|
+
import { BridgeError } from "./errors.js";
|
|
23
|
+
/**
|
|
24
|
+
* One long-lived connection to the Python server.
|
|
25
|
+
*
|
|
26
|
+
* Spawning per call would pay `uv`'s dependency resolution and a Photos library
|
|
27
|
+
* scan every time, which is seconds each. The process is started on first use
|
|
28
|
+
* and reused.
|
|
29
|
+
*/
|
|
30
|
+
export class PythonBridge {
|
|
31
|
+
config;
|
|
32
|
+
client;
|
|
33
|
+
starting;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
}
|
|
37
|
+
async connect() {
|
|
38
|
+
if (this.client)
|
|
39
|
+
return this.client;
|
|
40
|
+
if (this.starting)
|
|
41
|
+
return this.starting;
|
|
42
|
+
this.starting = (async () => {
|
|
43
|
+
const client = new Client({ name: "apple-photos-cli", version: "1.0.0" }, { capabilities: {} });
|
|
44
|
+
// `uv run` resolves osxphotos and photoscript on first use and caches
|
|
45
|
+
// them, so the reader needs neither a virtualenv nor a pip install.
|
|
46
|
+
const transport = new StdioClientTransport({
|
|
47
|
+
command: this.config.pythonCommand,
|
|
48
|
+
args: this.config.pythonArgs,
|
|
49
|
+
env: { ...process.env, ...this.config.pythonEnv },
|
|
50
|
+
stderr: "pipe",
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await client.connect(transport);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new BridgeError(`Could not start the Photos engine with \`${this.config.pythonCommand} ${this.config.pythonArgs.join(" ")}\`. ` +
|
|
57
|
+
`Install uv (https://docs.astral.sh/uv/) or set APPLE_PHOTOS_PYTHON to a Python that has osxphotos and photoscript. ` +
|
|
58
|
+
`Underlying error: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
this.client = client;
|
|
61
|
+
return client;
|
|
62
|
+
})();
|
|
63
|
+
try {
|
|
64
|
+
return await this.starting;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
this.starting = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Call one Python tool and return its text payload. */
|
|
71
|
+
async call(tool, args) {
|
|
72
|
+
const client = await this.connect();
|
|
73
|
+
const result = (await client.callTool({ name: tool, arguments: args }));
|
|
74
|
+
const text = (result.content ?? [])
|
|
75
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
76
|
+
.map((part) => part.text)
|
|
77
|
+
.join("\n");
|
|
78
|
+
return { text, isError: result.isError === true };
|
|
79
|
+
}
|
|
80
|
+
/** What the Python server says it offers. Used by `doctor` to prove the two agree. */
|
|
81
|
+
async listTools() {
|
|
82
|
+
const client = await this.connect();
|
|
83
|
+
const { tools } = await client.listTools();
|
|
84
|
+
return tools.map((tool) => tool.name);
|
|
85
|
+
}
|
|
86
|
+
async close() {
|
|
87
|
+
await this.client?.close().catch(() => undefined);
|
|
88
|
+
this.client = undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src-ts/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACN,MAAM,CAAS;IACxB,MAAM,CAAU;IAChB,QAAQ,CAAmB;IAEnC,YAAY,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;QACpC,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC;QAExC,IAAI,CAAC,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,EAC9C,EAAE,YAAY,EAAE,EAAE,EAAE,CACrB,CAAC;YAEF,sEAAsE;YACtE,oEAAoE;YACpE,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC;gBACzC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa;gBAClC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU;gBAC5B,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,EAA4B;gBAC3E,MAAM,EAAE,MAAM;aACf,CAAC,CAAC;YAEH,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAClC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,WAAW,CACnB,4CAA4C,IAAI,CAAC,MAAM,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM;oBAC7G,qHAAqH;oBACrH,qBAAsB,KAAe,CAAC,OAAO,EAAE,CAClD,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,EAAE,CAAC;QAEL,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC;QAC7B,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,wDAAwD;IACxD,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,IAA6B;QACpD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAEpC,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAGrE,CAAC;QAEF,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;aAChC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;aACvE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAc,CAAC;aAClC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEd,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;IACpD,CAAC;IAED,sFAAsF;IACtF,KAAK,CAAC,SAAS;QACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACpC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;IAC1B,CAAC;CACF"}
|
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* `register()` in tools/kit.ts turns a `ToolSpec` into an MCP tool. This turns
|
|
5
|
+
* the same spec into a shell command, from the same `ALL_TOOLS` array, through
|
|
6
|
+
* the same handler and the same `WriteGuard`. Nothing is described twice, so a
|
|
7
|
+
* tool added tomorrow is a command tomorrow and the two surfaces cannot drift.
|
|
8
|
+
*
|
|
9
|
+
* The command IS the tool name. `submit_imagine` runs as `submit-imagine`, and
|
|
10
|
+
* the underscore form works too. Inventing a prettier command tree would mean a
|
|
11
|
+
* hand-written mapping, which is exactly the drift this avoids, and it would
|
|
12
|
+
* force anyone reading the SKILL.md to learn two vocabularies for one action.
|
|
13
|
+
*
|
|
14
|
+
* Zod is the only schema: every flag, its placeholder, its help text and its
|
|
15
|
+
* validation come from the shape the MCP tool already declares.
|
|
16
|
+
*/
|
|
17
|
+
import { type ZodRawShape } from "zod";
|
|
18
|
+
import { type AnyToolSpec } from "./tools/kit.js";
|
|
19
|
+
/** How a value reaches the parser, once the Zod wrappers are peeled off. */
|
|
20
|
+
type FlagKind = "string" | "number" | "boolean" | "enum" | "json";
|
|
21
|
+
type Flag = {
|
|
22
|
+
key: string;
|
|
23
|
+
flag: string;
|
|
24
|
+
kind: FlagKind;
|
|
25
|
+
required: boolean;
|
|
26
|
+
repeatable: boolean;
|
|
27
|
+
choices?: string[];
|
|
28
|
+
help: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function flagsFor(shape: ZodRawShape): Flag[];
|
|
31
|
+
/**
|
|
32
|
+
* Midjourney's own parameter spellings, accepted as flags.
|
|
33
|
+
*
|
|
34
|
+
* The schema names things in full because a tool description is read by a model
|
|
35
|
+
* that has never seen a Midjourney prompt. A person at a terminal has, and they
|
|
36
|
+
* will type `--ar 16:9`, because that is what the parameter is called
|
|
37
|
+
* everywhere Midjourney documents it. Refusing the name the whole ecosystem
|
|
38
|
+
* uses, to protect a naming convention nobody outside this repo can see, would
|
|
39
|
+
* be the wrong trade.
|
|
40
|
+
*/
|
|
41
|
+
export declare const FLAG_ALIASES: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Parse argv against a tool's flags.
|
|
44
|
+
*
|
|
45
|
+
* Zod does the real validation afterwards, so this only has to get values into
|
|
46
|
+
* the right JavaScript types and catch the mistakes Zod would otherwise report
|
|
47
|
+
* in terms of a schema the person at the terminal never sees.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseArgs(argv: string[], flags: Flag[]): Record<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* Keep only the named fields.
|
|
52
|
+
*
|
|
53
|
+
* These endpoints are verbose: one explore page is tens of kilobytes of JSON,
|
|
54
|
+
* most of it layout hints. An agent piping that into its own context pays for
|
|
55
|
+
* every byte. `--select id,status,images` cuts it to what was asked for.
|
|
56
|
+
* Dotted paths descend, and arrays are traversed element-wise.
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectFields(data: unknown, paths: string[]): unknown;
|
|
59
|
+
/** Errors are JSON on stderr, always, so a caller parses one shape. */
|
|
60
|
+
export declare function emitError(error: unknown): void;
|
|
61
|
+
/**
|
|
62
|
+
* The name this was invoked as, so examples are copy-pasteable.
|
|
63
|
+
*
|
|
64
|
+
* The package puts two binaries on the same file. Printing `apple-photos-mcp` at
|
|
65
|
+
* someone who typed `apple-photos-cli` hands them a command that works but is not
|
|
66
|
+
* the one in their fingers.
|
|
67
|
+
*/
|
|
68
|
+
export declare function binName(): string;
|
|
69
|
+
/**
|
|
70
|
+
* Words people use mapped onto words the tools use.
|
|
71
|
+
*
|
|
72
|
+
* Without this, "make a picture" matches nothing, because every tool says
|
|
73
|
+
* "generate" and "image". A lookup that only works when you already know the
|
|
74
|
+
* vocabulary is not a lookup.
|
|
75
|
+
*/
|
|
76
|
+
export declare const SYNONYMS: Record<string, string[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a capability described in words to the command that does it.
|
|
79
|
+
*
|
|
80
|
+
* An agent that knows what it wants but not what this tool calls it would
|
|
81
|
+
* otherwise read the full tool list to find out, which is the cost the CLI
|
|
82
|
+
* exists to avoid. Scoring is deliberately dumb: name and title carry the most
|
|
83
|
+
* weight, because a tool called `download_job` should win "download my images"
|
|
84
|
+
* without any cleverness.
|
|
85
|
+
*/
|
|
86
|
+
export declare function whichCommand(query: string): {
|
|
87
|
+
tool: AnyToolSpec;
|
|
88
|
+
score: number;
|
|
89
|
+
}[];
|
|
90
|
+
export declare function isCliCommand(argv: string[]): boolean;
|
|
91
|
+
export declare function runCli(argv: string[]): Promise<number>;
|
|
92
|
+
export {};
|