@aernoud/contactsmcp 0.1.1
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/dist/index.d.ts +2 -0
- package/dist/index.js +70 -0
- package/dist/tools/addToGroup.d.ts +5 -0
- package/dist/tools/addToGroup.js +30 -0
- package/dist/tools/createContact.d.ts +8 -0
- package/dist/tools/createContact.js +25 -0
- package/dist/tools/listGroups.d.ts +3 -0
- package/dist/tools/listGroups.js +18 -0
- package/dist/tools/readContact.d.ts +19 -0
- package/dist/tools/readContact.js +80 -0
- package/dist/tools/searchContacts.d.ts +1 -0
- package/dist/tools/searchContacts.js +69 -0
- package/dist/tools/updateContact.d.ts +10 -0
- package/dist/tools/updateContact.js +49 -0
- package/package.json +18 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { searchContacts } from "./tools/searchContacts.js";
|
|
6
|
+
import { readContact } from "./tools/readContact.js";
|
|
7
|
+
import { createContact } from "./tools/createContact.js";
|
|
8
|
+
import { updateContact } from "./tools/updateContact.js";
|
|
9
|
+
import { listGroups } from "./tools/listGroups.js";
|
|
10
|
+
import { addToGroup } from "./tools/addToGroup.js";
|
|
11
|
+
const server = new McpServer({
|
|
12
|
+
name: "contactsmcp",
|
|
13
|
+
version: "0.1.0",
|
|
14
|
+
});
|
|
15
|
+
server.tool("search-contacts", "Search contacts in macOS Contacts.app by name, email, or phone number", {
|
|
16
|
+
query: z.string().describe("Search query — matches against name, email, and phone"),
|
|
17
|
+
limit: z.number().optional().default(25).describe("Max results to return"),
|
|
18
|
+
}, async ({ query, limit }) => {
|
|
19
|
+
const contacts = await searchContacts(query, limit);
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify(contacts, null, 2) }] };
|
|
21
|
+
});
|
|
22
|
+
server.tool("read-contact", "Get full details of a contact by their Contacts.app ID", {
|
|
23
|
+
contactId: z.string().describe("Contact ID from Contacts.app (from search results)"),
|
|
24
|
+
}, async ({ contactId }) => {
|
|
25
|
+
const contact = await readContact(contactId);
|
|
26
|
+
if (!contact) {
|
|
27
|
+
return { content: [{ type: "text", text: "Contact not found." }] };
|
|
28
|
+
}
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify(contact, null, 2) }] };
|
|
30
|
+
});
|
|
31
|
+
server.tool("create-contact", "Create a new contact in macOS Contacts.app", {
|
|
32
|
+
firstName: z.string().describe("First name"),
|
|
33
|
+
lastName: z.string().describe("Last name"),
|
|
34
|
+
email: z.string().optional().describe("Email address"),
|
|
35
|
+
phone: z.string().optional().describe("Phone number"),
|
|
36
|
+
organization: z.string().optional().describe("Organization / company name"),
|
|
37
|
+
}, async ({ firstName, lastName, email, phone, organization }) => {
|
|
38
|
+
const result = await createContact(firstName, lastName, email, phone, organization);
|
|
39
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
40
|
+
});
|
|
41
|
+
server.tool("update-contact", "Update fields on an existing contact in macOS Contacts.app", {
|
|
42
|
+
contactId: z.string().describe("Contact ID from Contacts.app"),
|
|
43
|
+
firstName: z.string().optional().describe("New first name"),
|
|
44
|
+
lastName: z.string().optional().describe("New last name"),
|
|
45
|
+
email: z.string().optional().describe("New primary email address"),
|
|
46
|
+
phone: z.string().optional().describe("New primary phone number"),
|
|
47
|
+
organization: z.string().optional().describe("New organization / company name"),
|
|
48
|
+
}, async ({ contactId, firstName, lastName, email, phone, organization }) => {
|
|
49
|
+
const result = await updateContact(contactId, firstName, lastName, email, phone, organization);
|
|
50
|
+
if (!result) {
|
|
51
|
+
return { content: [{ type: "text", text: "Contact not found." }] };
|
|
52
|
+
}
|
|
53
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
54
|
+
});
|
|
55
|
+
server.tool("list-groups", "List all contact groups in macOS Contacts.app with member counts", {}, async () => {
|
|
56
|
+
const groups = await listGroups();
|
|
57
|
+
return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }] };
|
|
58
|
+
});
|
|
59
|
+
server.tool("add-to-group", "Add a contact to a group in macOS Contacts.app", {
|
|
60
|
+
contactId: z.string().describe("Contact ID from Contacts.app"),
|
|
61
|
+
groupName: z.string().describe("Name of the group to add the contact to"),
|
|
62
|
+
}, async ({ contactId, groupName }) => {
|
|
63
|
+
const result = await addToGroup(contactId, groupName);
|
|
64
|
+
if (!result) {
|
|
65
|
+
return { content: [{ type: "text", text: "Contact not found." }] };
|
|
66
|
+
}
|
|
67
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
68
|
+
});
|
|
69
|
+
const transport = new StdioServerTransport();
|
|
70
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function addToGroup(contactId, groupName) {
|
|
3
|
+
const cId = escapeForAppleScript(contactId);
|
|
4
|
+
const gName = escapeForAppleScript(groupName);
|
|
5
|
+
const script = `
|
|
6
|
+
tell application "Contacts"
|
|
7
|
+
set personResults to (every person whose id is "${cId}")
|
|
8
|
+
if (count of personResults) = 0 then
|
|
9
|
+
return "CONTACT_NOT_FOUND"
|
|
10
|
+
end if
|
|
11
|
+
set groupResults to (every group whose name is "${gName}")
|
|
12
|
+
if (count of groupResults) = 0 then
|
|
13
|
+
return "GROUP_NOT_FOUND"
|
|
14
|
+
end if
|
|
15
|
+
set p to item 1 of personResults
|
|
16
|
+
set g to item 1 of groupResults
|
|
17
|
+
add p to g
|
|
18
|
+
save
|
|
19
|
+
return "done"
|
|
20
|
+
end tell`;
|
|
21
|
+
const result = await runAppleScript(script);
|
|
22
|
+
const trimmed = result.trim();
|
|
23
|
+
if (trimmed === "CONTACT_NOT_FOUND") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (trimmed === "GROUP_NOT_FOUND") {
|
|
27
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
28
|
+
}
|
|
29
|
+
return { contactId, groupName, status: "added" };
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function createContact(firstName: string, lastName: string, email?: string, phone?: string, organization?: string): Promise<{
|
|
2
|
+
id: string;
|
|
3
|
+
firstName: string;
|
|
4
|
+
lastName: string;
|
|
5
|
+
email: string | undefined;
|
|
6
|
+
phone: string | undefined;
|
|
7
|
+
organization: string | undefined;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function createContact(firstName, lastName, email, phone, organization) {
|
|
3
|
+
const fName = escapeForAppleScript(firstName);
|
|
4
|
+
const lName = escapeForAppleScript(lastName);
|
|
5
|
+
const emailBlock = email
|
|
6
|
+
? `make new email at end of emails of newPerson with properties {label:"work", value:"${escapeForAppleScript(email)}"}`
|
|
7
|
+
: "";
|
|
8
|
+
const phoneBlock = phone
|
|
9
|
+
? `make new phone at end of phones of newPerson with properties {label:"mobile", value:"${escapeForAppleScript(phone)}"}`
|
|
10
|
+
: "";
|
|
11
|
+
const orgBlock = organization
|
|
12
|
+
? `set organization of newPerson to "${escapeForAppleScript(organization)}"`
|
|
13
|
+
: "";
|
|
14
|
+
const script = `
|
|
15
|
+
tell application "Contacts"
|
|
16
|
+
set newPerson to make new person with properties {first name:"${fName}", last name:"${lName}"}
|
|
17
|
+
${emailBlock}
|
|
18
|
+
${phoneBlock}
|
|
19
|
+
${orgBlock}
|
|
20
|
+
save
|
|
21
|
+
return id of newPerson
|
|
22
|
+
end tell`;
|
|
23
|
+
const id = await runAppleScript(script);
|
|
24
|
+
return { id: id.trim(), firstName, lastName, email, phone, organization };
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runAppleScript, parseRecords, FIELD_SEP, RECORD_SEP } from "@mailappmcp/shared";
|
|
2
|
+
export async function listGroups() {
|
|
3
|
+
const script = `
|
|
4
|
+
tell application "Contacts"
|
|
5
|
+
set output to ""
|
|
6
|
+
repeat with g in every group
|
|
7
|
+
set gCount to 0
|
|
8
|
+
try
|
|
9
|
+
set gCount to count of people of g
|
|
10
|
+
end try
|
|
11
|
+
set output to output & (id of g) & "${FIELD_SEP}" & (name of g) & "${FIELD_SEP}" & gCount & "${RECORD_SEP}"
|
|
12
|
+
end repeat
|
|
13
|
+
return output
|
|
14
|
+
end tell`;
|
|
15
|
+
const raw = await runAppleScript(script);
|
|
16
|
+
const records = parseRecords(raw, ["id", "name", "memberCount"]);
|
|
17
|
+
return records.map((r) => ({ ...r, memberCount: parseInt(r.memberCount, 10) || 0 }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function readContact(contactId: string): Promise<{
|
|
2
|
+
name: string;
|
|
3
|
+
firstName: string;
|
|
4
|
+
lastName: string;
|
|
5
|
+
organization: string;
|
|
6
|
+
emails: {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}[];
|
|
10
|
+
phones: {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}[];
|
|
14
|
+
addresses: {
|
|
15
|
+
label: string;
|
|
16
|
+
value: string;
|
|
17
|
+
}[];
|
|
18
|
+
note: string;
|
|
19
|
+
} | null>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript, FIELD_SEP } from "@mailappmcp/shared";
|
|
2
|
+
export async function readContact(contactId) {
|
|
3
|
+
const cId = escapeForAppleScript(contactId);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
set results to (every person whose id is "${cId}")
|
|
7
|
+
if (count of results) = 0 then
|
|
8
|
+
return "NOT_FOUND"
|
|
9
|
+
end if
|
|
10
|
+
set p to item 1 of results
|
|
11
|
+
set pName to ""
|
|
12
|
+
try
|
|
13
|
+
set pName to name of p
|
|
14
|
+
end try
|
|
15
|
+
set pFirst to ""
|
|
16
|
+
try
|
|
17
|
+
set pFirst to first name of p as string
|
|
18
|
+
if pFirst is "missing value" then set pFirst to ""
|
|
19
|
+
end try
|
|
20
|
+
set pLast to ""
|
|
21
|
+
try
|
|
22
|
+
set pLast to last name of p as string
|
|
23
|
+
if pLast is "missing value" then set pLast to ""
|
|
24
|
+
end try
|
|
25
|
+
set pOrg to ""
|
|
26
|
+
try
|
|
27
|
+
set pOrg to organization of p as string
|
|
28
|
+
if pOrg is "missing value" then set pOrg to ""
|
|
29
|
+
end try
|
|
30
|
+
set emailList to ""
|
|
31
|
+
try
|
|
32
|
+
repeat with e in emails of p
|
|
33
|
+
set emailList to emailList & (label of e) & ":" & (value of e) & ","
|
|
34
|
+
end repeat
|
|
35
|
+
end try
|
|
36
|
+
set phoneList to ""
|
|
37
|
+
try
|
|
38
|
+
repeat with ph in phones of p
|
|
39
|
+
set phoneList to phoneList & (label of ph) & ":" & (value of ph) & ","
|
|
40
|
+
end repeat
|
|
41
|
+
end try
|
|
42
|
+
set addrList to ""
|
|
43
|
+
try
|
|
44
|
+
repeat with a in addresses of p
|
|
45
|
+
set addrList to addrList & (label of a) & ":" & (formatted address of a) & ","
|
|
46
|
+
end repeat
|
|
47
|
+
end try
|
|
48
|
+
set pNote to ""
|
|
49
|
+
try
|
|
50
|
+
set pNote to note of p as string
|
|
51
|
+
if pNote is "missing value" then set pNote to ""
|
|
52
|
+
end try
|
|
53
|
+
return pName & "${FIELD_SEP}" & pFirst & "${FIELD_SEP}" & pLast & "${FIELD_SEP}" & pOrg & "${FIELD_SEP}" & emailList & "${FIELD_SEP}" & phoneList & "${FIELD_SEP}" & addrList & "${FIELD_SEP}" & pNote
|
|
54
|
+
end tell`;
|
|
55
|
+
const raw = await runAppleScript(script);
|
|
56
|
+
if (raw.trim() === "NOT_FOUND") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const parts = raw.split(FIELD_SEP);
|
|
60
|
+
const parseList = (s) => (s ?? "")
|
|
61
|
+
.trim()
|
|
62
|
+
.split(",")
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.map((item) => {
|
|
65
|
+
const colonIdx = item.indexOf(":");
|
|
66
|
+
if (colonIdx === -1)
|
|
67
|
+
return { label: "", value: item };
|
|
68
|
+
return { label: item.slice(0, colonIdx), value: item.slice(colonIdx + 1) };
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
name: (parts[0] ?? "").trim(),
|
|
72
|
+
firstName: (parts[1] ?? "").trim(),
|
|
73
|
+
lastName: (parts[2] ?? "").trim(),
|
|
74
|
+
organization: (parts[3] ?? "").trim(),
|
|
75
|
+
emails: parseList(parts[4] ?? ""),
|
|
76
|
+
phones: parseList(parts[5] ?? ""),
|
|
77
|
+
addresses: parseList(parts[6] ?? ""),
|
|
78
|
+
note: (parts[7] ?? "").trim(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function searchContacts(query: string, limit?: number): Promise<Record<string, string>[]>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript, parseRecords, FIELD_SEP, RECORD_SEP } from "@mailappmcp/shared";
|
|
2
|
+
export async function searchContacts(query, limit = 25) {
|
|
3
|
+
const q = escapeForAppleScript(query);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
set matchedPeople to {}
|
|
7
|
+
try
|
|
8
|
+
set nameMatches to (every person whose name contains "${q}")
|
|
9
|
+
repeat with p in nameMatches
|
|
10
|
+
set end of matchedPeople to p
|
|
11
|
+
end repeat
|
|
12
|
+
end try
|
|
13
|
+
if (count of matchedPeople) = 0 then
|
|
14
|
+
try
|
|
15
|
+
set allPeople to every person
|
|
16
|
+
repeat with p in allPeople
|
|
17
|
+
set found to false
|
|
18
|
+
repeat with e in emails of p
|
|
19
|
+
if value of e contains "${q}" then
|
|
20
|
+
set found to true
|
|
21
|
+
exit repeat
|
|
22
|
+
end if
|
|
23
|
+
end repeat
|
|
24
|
+
if not found then
|
|
25
|
+
repeat with ph in phones of p
|
|
26
|
+
if value of ph contains "${q}" then
|
|
27
|
+
set found to true
|
|
28
|
+
exit repeat
|
|
29
|
+
end if
|
|
30
|
+
end repeat
|
|
31
|
+
end if
|
|
32
|
+
if found then
|
|
33
|
+
set end of matchedPeople to p
|
|
34
|
+
end if
|
|
35
|
+
end repeat
|
|
36
|
+
end try
|
|
37
|
+
end if
|
|
38
|
+
set output to ""
|
|
39
|
+
set maxCount to ${limit}
|
|
40
|
+
set i to 0
|
|
41
|
+
repeat with p in matchedPeople
|
|
42
|
+
if i >= maxCount then exit repeat
|
|
43
|
+
set pId to id of p
|
|
44
|
+
set pName to name of p
|
|
45
|
+
set pOrg to ""
|
|
46
|
+
try
|
|
47
|
+
set pOrg to organization of p as string
|
|
48
|
+
end try
|
|
49
|
+
if pOrg is "missing value" then set pOrg to ""
|
|
50
|
+
set pEmail to ""
|
|
51
|
+
try
|
|
52
|
+
if (count of emails of p) > 0 then
|
|
53
|
+
set pEmail to value of first email of p
|
|
54
|
+
end if
|
|
55
|
+
end try
|
|
56
|
+
set pPhone to ""
|
|
57
|
+
try
|
|
58
|
+
if (count of phones of p) > 0 then
|
|
59
|
+
set pPhone to value of first phone of p
|
|
60
|
+
end if
|
|
61
|
+
end try
|
|
62
|
+
set output to output & pId & "${FIELD_SEP}" & pName & "${FIELD_SEP}" & pOrg & "${FIELD_SEP}" & pEmail & "${FIELD_SEP}" & pPhone & "${RECORD_SEP}"
|
|
63
|
+
set i to i + 1
|
|
64
|
+
end repeat
|
|
65
|
+
return output
|
|
66
|
+
end tell`;
|
|
67
|
+
const raw = await runAppleScript(script);
|
|
68
|
+
return parseRecords(raw, ["id", "name", "organization", "email", "phone"]);
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function updateContact(contactId: string, firstName?: string, lastName?: string, email?: string, phone?: string, organization?: string): Promise<{
|
|
2
|
+
contactId: string;
|
|
3
|
+
updated: {
|
|
4
|
+
firstName: string | undefined;
|
|
5
|
+
lastName: string | undefined;
|
|
6
|
+
email: string | undefined;
|
|
7
|
+
phone: string | undefined;
|
|
8
|
+
organization: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
} | null>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function updateContact(contactId, firstName, lastName, email, phone, organization) {
|
|
3
|
+
const cId = escapeForAppleScript(contactId);
|
|
4
|
+
const firstNameBlock = firstName !== undefined
|
|
5
|
+
? `set first name of p to "${escapeForAppleScript(firstName)}"`
|
|
6
|
+
: "";
|
|
7
|
+
const lastNameBlock = lastName !== undefined
|
|
8
|
+
? `set last name of p to "${escapeForAppleScript(lastName)}"`
|
|
9
|
+
: "";
|
|
10
|
+
const orgBlock = organization !== undefined
|
|
11
|
+
? `set organization of p to "${escapeForAppleScript(organization)}"`
|
|
12
|
+
: "";
|
|
13
|
+
const emailBlock = email !== undefined
|
|
14
|
+
? `
|
|
15
|
+
if (count of emails of p) > 0 then
|
|
16
|
+
set value of first email of p to "${escapeForAppleScript(email)}"
|
|
17
|
+
else
|
|
18
|
+
make new email at end of emails of p with properties {label:"work", value:"${escapeForAppleScript(email)}"}
|
|
19
|
+
end if`
|
|
20
|
+
: "";
|
|
21
|
+
const phoneBlock = phone !== undefined
|
|
22
|
+
? `
|
|
23
|
+
if (count of phones of p) > 0 then
|
|
24
|
+
set value of first phone of p to "${escapeForAppleScript(phone)}"
|
|
25
|
+
else
|
|
26
|
+
make new phone at end of phones of p with properties {label:"mobile", value:"${escapeForAppleScript(phone)}"}
|
|
27
|
+
end if`
|
|
28
|
+
: "";
|
|
29
|
+
const script = `
|
|
30
|
+
tell application "Contacts"
|
|
31
|
+
set results to (every person whose id is "${cId}")
|
|
32
|
+
if (count of results) = 0 then
|
|
33
|
+
return "NOT_FOUND"
|
|
34
|
+
end if
|
|
35
|
+
set p to item 1 of results
|
|
36
|
+
${firstNameBlock}
|
|
37
|
+
${lastNameBlock}
|
|
38
|
+
${orgBlock}
|
|
39
|
+
${emailBlock}
|
|
40
|
+
${phoneBlock}
|
|
41
|
+
save
|
|
42
|
+
return "ok"
|
|
43
|
+
end tell`;
|
|
44
|
+
const result = await runAppleScript(script);
|
|
45
|
+
if (result.trim() === "NOT_FOUND") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { contactId, updated: { firstName, lastName, email, phone, organization } };
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aernoud/contactsmcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for macOS Contacts.app — contact management via AppleScript",
|
|
5
|
+
"mcpName": "io.github.aernouddekker/contactsmcp",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "contactsmcp": "./dist/index.js" },
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"scripts": { "build": "tsc", "start": "node dist/index.js", "prepublishOnly": "npm run build" },
|
|
10
|
+
"keywords": ["mcp", "mcp-server", "contacts", "address-book", "macos", "applescript", "claude"],
|
|
11
|
+
"author": "Aernoud Dekker",
|
|
12
|
+
"repository": { "type": "git", "url": "https://github.com/aernouddekker/macos-mcp.git", "directory": "packages/contacts" },
|
|
13
|
+
"dependencies": { "@mailappmcp/shared": "*", "@modelcontextprotocol/sdk": "^1.12.0", "zod": "^3.23.0" },
|
|
14
|
+
"devDependencies": { "typescript": "^5.5.0", "@types/node": "^22.0.0" },
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"engines": { "node": ">=18" },
|
|
17
|
+
"os": ["darwin"]
|
|
18
|
+
}
|