diff --git a/src/index.ts b/src/index.ts
index aef9d75b16..b2dd705f0b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -223,6 +223,7 @@ export {
   parseValue,
   parseConstValue,
   parseType,
+  parseSchemaCoordinate,
   // Print
   print,
   // Visit
@@ -243,6 +244,7 @@ export {
   isTypeDefinitionNode,
   isTypeSystemExtensionNode,
   isTypeExtensionNode,
+  isSchemaCoordinateNode,
 } from './language/index.js';
 
 export type {
@@ -315,6 +317,7 @@ export type {
   UnionTypeExtensionNode,
   EnumTypeExtensionNode,
   InputObjectTypeExtensionNode,
+  SchemaCoordinateNode,
 } from './language/index.js';
 
 // Execute GraphQL queries.
@@ -482,6 +485,8 @@ export {
   findBreakingChanges,
   findDangerousChanges,
   findSchemaChanges,
+  resolveSchemaCoordinate,
+  resolveASTSchemaCoordinate,
 } from './utilities/index.js';
 
 export type {
@@ -512,4 +517,5 @@ export type {
   SafeChange,
   DangerousChange,
   TypedQueryDocumentNode,
+  ResolvedSchemaElement,
 } from './utilities/index.js';
diff --git a/src/language/__tests__/lexer-test.ts b/src/language/__tests__/lexer-test.ts
index 85603dfaaa..14357eadbe 100644
--- a/src/language/__tests__/lexer-test.ts
+++ b/src/language/__tests__/lexer-test.ts
@@ -165,13 +165,6 @@ describe('Lexer', () => {
     });
   });
 
-  it('reports unexpected characters', () => {
-    expectSyntaxError('.').to.deep.equal({
-      message: 'Syntax Error: Unexpected character: ".".',
-      locations: [{ line: 1, column: 1 }],
-    });
-  });
-
   it('errors respect whitespace', () => {
     let caughtError;
     try {
@@ -965,6 +958,13 @@ describe('Lexer', () => {
       value: undefined,
     });
 
+    expect(lexOne('.')).to.contain({
+      kind: TokenKind.DOT,
+      start: 0,
+      end: 1,
+      value: undefined,
+    });
+
     expect(lexOne('...')).to.contain({
       kind: TokenKind.SPREAD,
       start: 0,
diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts
index d98b6a6f41..786eab3324 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -11,7 +11,13 @@ import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';
 import { inspect } from '../../jsutils/inspect.js';
 
 import { Kind } from '../kinds.js';
-import { parse, parseConstValue, parseType, parseValue } from '../parser.js';
+import {
+  parse,
+  parseConstValue,
+  parseSchemaCoordinate,
+  parseType,
+  parseValue,
+} from '../parser.js';
 import { Source } from '../source.js';
 import { TokenKind } from '../tokenKind.js';
 
@@ -679,4 +685,129 @@ describe('Parser', () => {
       });
     });
   });
+
+  describe('parseSchemaCoordinate', () => {
+    it('parses Name', () => {
+      const result = parseSchemaCoordinate('MyType');
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SCHEMA_COORDINATE,
+        loc: { start: 0, end: 6 },
+        ofDirective: false,
+        name: {
+          kind: Kind.NAME,
+          loc: { start: 0, end: 6 },
+          value: 'MyType',
+        },
+        memberName: undefined,
+        argumentName: undefined,
+      });
+    });
+
+    it('parses Name . Name', () => {
+      const result = parseSchemaCoordinate('MyType.field');
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SCHEMA_COORDINATE,
+        loc: { start: 0, end: 12 },
+        ofDirective: false,
+        name: {
+          kind: Kind.NAME,
+          loc: { start: 0, end: 6 },
+          value: 'MyType',
+        },
+        memberName: {
+          kind: Kind.NAME,
+          loc: { start: 7, end: 12 },
+          value: 'field',
+        },
+        argumentName: undefined,
+      });
+    });
+
+    it('rejects Name . Name . Name', () => {
+      expectToThrowJSON(() =>
+        parseSchemaCoordinate('MyType.field.deep'),
+      ).to.deep.equal({
+        message: 'Syntax Error: Expected <EOF>, found ".".',
+        locations: [{ line: 1, column: 13 }],
+      });
+    });
+
+    it('parses Name . Name ( Name : )', () => {
+      const result = parseSchemaCoordinate('MyType.field(arg:)');
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SCHEMA_COORDINATE,
+        loc: { start: 0, end: 18 },
+        ofDirective: false,
+        name: {
+          kind: Kind.NAME,
+          loc: { start: 0, end: 6 },
+          value: 'MyType',
+        },
+        memberName: {
+          kind: Kind.NAME,
+          loc: { start: 7, end: 12 },
+          value: 'field',
+        },
+        argumentName: {
+          kind: Kind.NAME,
+          loc: { start: 13, end: 16 },
+          value: 'arg',
+        },
+      });
+    });
+
+    it('rejects Name . Name ( Name : Name )', () => {
+      expectToThrowJSON(() =>
+        parseSchemaCoordinate('MyType.field(arg: value)'),
+      ).to.deep.equal({
+        message: 'Syntax Error: Expected ")", found Name "value".',
+        locations: [{ line: 1, column: 19 }],
+      });
+    });
+
+    it('parses @ Name', () => {
+      const result = parseSchemaCoordinate('@myDirective');
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SCHEMA_COORDINATE,
+        loc: { start: 0, end: 12 },
+        ofDirective: true,
+        name: {
+          kind: Kind.NAME,
+          loc: { start: 1, end: 12 },
+          value: 'myDirective',
+        },
+        memberName: undefined,
+        argumentName: undefined,
+      });
+    });
+
+    it('parses @ Name ( Name : )', () => {
+      const result = parseSchemaCoordinate('@myDirective(arg:)');
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SCHEMA_COORDINATE,
+        loc: { start: 0, end: 18 },
+        ofDirective: true,
+        name: {
+          kind: Kind.NAME,
+          loc: { start: 1, end: 12 },
+          value: 'myDirective',
+        },
+        memberName: undefined,
+        argumentName: {
+          kind: Kind.NAME,
+          loc: { start: 13, end: 16 },
+          value: 'arg',
+        },
+      });
+    });
+
+    it('rejects @ Name . Name', () => {
+      expectToThrowJSON(() =>
+        parseSchemaCoordinate('@myDirective.field'),
+      ).to.deep.equal({
+        message: 'Syntax Error: Expected <EOF>, found ".".',
+        locations: [{ line: 1, column: 13 }],
+      });
+    });
+  });
 });
diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts
index 350e3f1a6b..b15d5f7319 100644
--- a/src/language/__tests__/predicates-test.ts
+++ b/src/language/__tests__/predicates-test.ts
@@ -8,6 +8,7 @@ import {
   isConstValueNode,
   isDefinitionNode,
   isExecutableDefinitionNode,
+  isSchemaCoordinateNode,
   isSelectionNode,
   isTypeDefinitionNode,
   isTypeExtensionNode,
@@ -141,4 +142,10 @@ describe('AST node predicates', () => {
       'InputObjectTypeExtension',
     ]);
   });
+
+  it('isSchemaCoordinateNode', () => {
+    expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([
+      'SchemaCoordinate',
+    ]);
+  });
 });
diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts
index 624dc75ca2..589d9bfc8d 100644
--- a/src/language/__tests__/printer-test.ts
+++ b/src/language/__tests__/printer-test.ts
@@ -5,7 +5,7 @@ import { dedent, dedentString } from '../../__testUtils__/dedent.js';
 import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';
 
 import { Kind } from '../kinds.js';
-import { parse } from '../parser.js';
+import { parse, parseSchemaCoordinate } from '../parser.js';
 import { print } from '../printer.js';
 
 describe('Printer: Query document', () => {
@@ -299,4 +299,18 @@ describe('Printer: Query document', () => {
     `),
     );
   });
+
+  it('prints schema coordinates', () => {
+    expect(print(parseSchemaCoordinate('  Name  '))).to.equal('Name');
+    expect(print(parseSchemaCoordinate('  Name . field '))).to.equal(
+      'Name.field',
+    );
+    expect(print(parseSchemaCoordinate('  Name . field ( arg: )'))).to.equal(
+      'Name.field(arg:)',
+    );
+    expect(print(parseSchemaCoordinate(' @ name  '))).to.equal('@name');
+    expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
+      '@name(arg:)',
+    );
+  });
 });
diff --git a/src/language/ast.ts b/src/language/ast.ts
index bce69a6e19..b76e082f48 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -181,7 +181,8 @@ export type ASTNode =
   | InterfaceTypeExtensionNode
   | UnionTypeExtensionNode
   | EnumTypeExtensionNode
-  | InputObjectTypeExtensionNode;
+  | InputObjectTypeExtensionNode
+  | SchemaCoordinateNode;
 
 /**
  * Utility type listing all nodes indexed by their kind.
@@ -287,6 +288,8 @@ export const QueryDocumentKeys: {
   UnionTypeExtension: ['name', 'directives', 'types'],
   EnumTypeExtension: ['name', 'directives', 'values'],
   InputObjectTypeExtension: ['name', 'directives', 'fields'],
+
+  SchemaCoordinate: ['name', 'memberName', 'argumentName'],
 };
 
 const kindValues = new Set<string>(Object.keys(QueryDocumentKeys));
@@ -762,3 +765,14 @@ export interface InputObjectTypeExtensionNode {
   readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
   readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
 }
+
+/** Schema Coordinates */
+
+export interface SchemaCoordinateNode {
+  readonly kind: typeof Kind.SCHEMA_COORDINATE;
+  readonly loc?: Location | undefined;
+  readonly ofDirective: boolean;
+  readonly name: NameNode;
+  readonly memberName?: NameNode | undefined;
+  readonly argumentName?: NameNode | undefined;
+}
diff --git a/src/language/index.ts b/src/language/index.ts
index 4d96766abc..a0d8a02cc5 100644
--- a/src/language/index.ts
+++ b/src/language/index.ts
@@ -11,7 +11,13 @@ export { TokenKind } from './tokenKind.js';
 
 export { Lexer } from './lexer.js';
 
-export { parse, parseValue, parseConstValue, parseType } from './parser.js';
+export {
+  parse,
+  parseValue,
+  parseConstValue,
+  parseType,
+  parseSchemaCoordinate,
+} from './parser.js';
 export type { ParseOptions } from './parser.js';
 
 export { print } from './printer.js';
@@ -88,6 +94,7 @@ export type {
   UnionTypeExtensionNode,
   EnumTypeExtensionNode,
   InputObjectTypeExtensionNode,
+  SchemaCoordinateNode,
 } from './ast.js';
 
 export {
@@ -101,6 +108,7 @@ export {
   isTypeDefinitionNode,
   isTypeSystemExtensionNode,
   isTypeExtensionNode,
+  isSchemaCoordinateNode,
 } from './predicates.js';
 
 export { DirectiveLocation } from './directiveLocation.js';
diff --git a/src/language/kinds.ts b/src/language/kinds.ts
index d41eccecfc..fabc5642e5 100644
--- a/src/language/kinds.ts
+++ b/src/language/kinds.ts
@@ -67,6 +67,9 @@ export const Kind = {
   UNION_TYPE_EXTENSION: 'UnionTypeExtension' as const,
   ENUM_TYPE_EXTENSION: 'EnumTypeExtension' as const,
   INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension' as const,
+
+  /** Schema Coordinates */
+  SCHEMA_COORDINATE: 'SchemaCoordinate' as const,
 };
 // eslint-disable-next-line @typescript-eslint/no-redeclare
 export type Kind = (typeof Kind)[keyof typeof Kind];
diff --git a/src/language/lexer.ts b/src/language/lexer.ts
index 300aeebbdf..7ce6ec0c36 100644
--- a/src/language/lexer.ts
+++ b/src/language/lexer.ts
@@ -95,6 +95,7 @@ export function isPunctuatorTokenKind(kind: TokenKind): boolean {
     kind === TokenKind.AMP ||
     kind === TokenKind.PAREN_L ||
     kind === TokenKind.PAREN_R ||
+    kind === TokenKind.DOT ||
     kind === TokenKind.SPREAD ||
     kind === TokenKind.COLON ||
     kind === TokenKind.EQUALS ||
@@ -246,7 +247,11 @@ function readNextToken(lexer: Lexer, start: number): Token {
       //   - FloatValue
       //   - StringValue
       //
-      // Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | }
+      // Punctuator ::
+      //   - DotPunctuator
+      //   - OtherPunctuator
+      //
+      // OtherPunctuator :: one of ! $ & ( ) ... : = @ [ ] { | }
       case 0x0021: // !
         return createToken(lexer, TokenKind.BANG, position, position + 1);
       case 0x0024: // $
@@ -263,24 +268,7 @@ function readNextToken(lexer: Lexer, start: number): Token {
         if (nextCode === 0x002e && body.charCodeAt(position + 2) === 0x002e) {
           return createToken(lexer, TokenKind.SPREAD, position, position + 3);
         }
-        if (nextCode === 0x002e) {
-          throw syntaxError(
-            lexer.source,
-            position,
-            'Unexpected "..", did you mean "..."?',
-          );
-        } else if (isDigit(nextCode)) {
-          const digits = lexer.source.body.slice(
-            position + 1,
-            readDigits(lexer, position + 1, nextCode),
-          );
-          throw syntaxError(
-            lexer.source,
-            position,
-            `Invalid number, expected digit before ".", did you mean "0.${digits}"?`,
-          );
-        }
-        break;
+        return readDot(lexer, position);
       }
       case 0x003a: // :
         return createToken(lexer, TokenKind.COLON, position, position + 1);
@@ -333,6 +321,37 @@ function readNextToken(lexer: Lexer, start: number): Token {
   return createToken(lexer, TokenKind.EOF, bodyLength, bodyLength);
 }
 
+/**
+ * Reads a dot token with helpful messages for negative lookahead.
+ *
+ * ```
+ * DotPunctuator :: `.` [lookahead != {`.`, Digit}]
+ * ```
+ */
+function readDot(lexer: Lexer, start: number): Token {
+  const nextCode = lexer.source.body.charCodeAt(start + 1);
+  // Full Stop (.)
+  if (nextCode === 0x002e) {
+    throw syntaxError(
+      lexer.source,
+      start,
+      'Unexpected "..", did you mean "..."?',
+    );
+  }
+  if (isDigit(nextCode)) {
+    const digits = lexer.source.body.slice(
+      start + 1,
+      readDigits(lexer, start + 1, nextCode),
+    );
+    throw syntaxError(
+      lexer.source,
+      start,
+      `Invalid number, expected digit before ".", did you mean "0.${digits}"?`,
+    );
+  }
+  return createToken(lexer, TokenKind.DOT, start, start + 1);
+}
+
 /**
  * Reads a comment token from the source file.
  *
diff --git a/src/language/parser.ts b/src/language/parser.ts
index 3d2018ba4b..4684b339fb 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -47,6 +47,7 @@ import type {
   OperationTypeDefinitionNode,
   ScalarTypeDefinitionNode,
   ScalarTypeExtensionNode,
+  SchemaCoordinateNode,
   SchemaDefinitionNode,
   SchemaExtensionNode,
   SelectionNode,
@@ -182,6 +183,26 @@ export function parseType(
   return type;
 }
 
+/**
+ * Given a string containing a GraphQL Schema Coordinate (ex. `Type.field`),
+ * parse the AST for that schema coordinate.
+ * Throws GraphQLError if a syntax error is encountered.
+ *
+ * Consider providing the results to the utility function:
+ * resolveASTSchemaCoordinate(). Or calling resolveSchemaCoordinate() directly
+ * with an unparsed source.
+ */
+export function parseSchemaCoordinate(
+  source: string | Source,
+  options?: ParseOptions,
+): SchemaCoordinateNode {
+  const parser = new Parser(source, options);
+  parser.expectToken(TokenKind.SOF);
+  const type = parser.parseSchemaCoordinate();
+  parser.expectToken(TokenKind.EOF);
+  return type;
+}
+
 /**
  * This class is exported only to assist people in implementing their own parsers
  * without duplicating too much code and should be used only as last resort for cases
@@ -1433,6 +1454,44 @@ export class Parser {
     throw this.unexpected(start);
   }
 
+  // Schema Coordinates
+
+  /**
+   * ```
+   * SchemaCoordinate :
+   *   - Name
+   *   - Name . Name
+   *   - Name . Name ( Name : )
+   *   - @ Name
+   *   - @ Name ( Name : )
+   * ```
+   */
+  parseSchemaCoordinate(): SchemaCoordinateNode {
+    const start = this._lexer.token;
+    const ofDirective = this.expectOptionalToken(TokenKind.AT);
+    const name = this.parseName();
+    let memberName;
+    if (!ofDirective && this.expectOptionalToken(TokenKind.DOT)) {
+      memberName = this.parseName();
+    }
+    let argumentName;
+    if (
+      (ofDirective || memberName) &&
+      this.expectOptionalToken(TokenKind.PAREN_L)
+    ) {
+      argumentName = this.parseName();
+      this.expectToken(TokenKind.COLON);
+      this.expectToken(TokenKind.PAREN_R);
+    }
+    return this.node<SchemaCoordinateNode>(start, {
+      kind: Kind.SCHEMA_COORDINATE,
+      ofDirective,
+      name,
+      memberName,
+      argumentName,
+    });
+  }
+
   // Core parsing utility functions
 
   /**
diff --git a/src/language/predicates.ts b/src/language/predicates.ts
index 29ad5bf289..fa5923b90d 100644
--- a/src/language/predicates.ts
+++ b/src/language/predicates.ts
@@ -3,6 +3,7 @@ import type {
   ConstValueNode,
   DefinitionNode,
   ExecutableDefinitionNode,
+  SchemaCoordinateNode,
   SelectionNode,
   TypeDefinitionNode,
   TypeExtensionNode,
@@ -110,3 +111,9 @@ export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode {
     node.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION
   );
 }
+
+export function isSchemaCoordinateNode(
+  node: ASTNode,
+): node is SchemaCoordinateNode {
+  return node.kind === Kind.SCHEMA_COORDINATE;
+}
diff --git a/src/language/printer.ts b/src/language/printer.ts
index bf2959e59c..126701a011 100644
--- a/src/language/printer.ts
+++ b/src/language/printer.ts
@@ -320,6 +320,18 @@ const printDocASTReducer: ASTReducer<string> = {
     leave: ({ name, directives, fields }) =>
       join(['extend input', name, join(directives, ' '), block(fields)], ' '),
   },
+
+  // Schema Coordinate
+
+  SchemaCoordinate: {
+    leave: ({ ofDirective, name, memberName, argumentName }) =>
+      join([
+        ofDirective ? '@' : undefined,
+        name,
+        wrap('.', memberName),
+        wrap('(', argumentName, ':)'),
+      ]),
+  },
 };
 
 /**
diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts
index d1c7129b04..eae0972b81 100644
--- a/src/language/tokenKind.ts
+++ b/src/language/tokenKind.ts
@@ -10,6 +10,7 @@ export const TokenKind = {
   AMP: '&' as const,
   PAREN_L: '(' as const,
   PAREN_R: ')' as const,
+  DOT: '.' as const,
   SPREAD: '...' as const,
   COLON: ':' as const,
   EQUALS: '=' as const,
diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts
new file mode 100644
index 0000000000..e316ef52a1
--- /dev/null
+++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts
@@ -0,0 +1,185 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import type {
+  GraphQLEnumType,
+  GraphQLInputObjectType,
+  GraphQLObjectType,
+} from '../../type/definition.js';
+import type { GraphQLDirective } from '../../type/directives.js';
+
+import { buildSchema } from '../buildASTSchema.js';
+import { resolveSchemaCoordinate } from '../resolveSchemaCoordinate.js';
+
+describe('resolveSchemaCoordinate', () => {
+  const schema = buildSchema(`
+    type Query {
+      searchBusiness(criteria: SearchCriteria!): [Business]
+    }
+
+    input SearchCriteria {
+      name: String
+      filter: SearchFilter
+    }
+
+    enum SearchFilter {
+      OPEN_NOW
+      DELIVERS_TAKEOUT
+      VEGETARIAN_MENU
+    }
+
+    type Business {
+      id: ID
+      name: String
+      email: String @private(scope: "loggedIn")
+    }
+
+    directive @private(scope: String!) on FIELD_DEFINITION
+  `);
+
+  it('resolves a Named Type', () => {
+    expect(resolveSchemaCoordinate(schema, 'Business')).to.deep.equal({
+      kind: 'NamedType',
+      type: schema.getType('Business'),
+    });
+
+    expect(resolveSchemaCoordinate(schema, 'String')).to.deep.equal({
+      kind: 'NamedType',
+      type: schema.getType('String'),
+    });
+
+    expect(resolveSchemaCoordinate(schema, 'private')).to.deep.equal(undefined);
+
+    expect(resolveSchemaCoordinate(schema, 'Unknown')).to.deep.equal(undefined);
+  });
+
+  it('resolves a Type Field', () => {
+    const type = schema.getType('Business') as GraphQLObjectType;
+    const field = type.getFields().name;
+    expect(resolveSchemaCoordinate(schema, 'Business.name')).to.deep.equal({
+      kind: 'Field',
+      type,
+      field,
+    });
+
+    expect(resolveSchemaCoordinate(schema, 'Business.unknown')).to.deep.equal(
+      undefined,
+    );
+
+    expect(resolveSchemaCoordinate(schema, 'Unknown.field')).to.deep.equal(
+      undefined,
+    );
+
+    expect(resolveSchemaCoordinate(schema, 'String.field')).to.deep.equal(
+      undefined,
+    );
+  });
+
+  it('does not resolve meta-fields', () => {
+    expect(
+      resolveSchemaCoordinate(schema, 'Business.__typename'),
+    ).to.deep.equal(undefined);
+  });
+
+  it('resolves a Input Field', () => {
+    const type = schema.getType('SearchCriteria') as GraphQLInputObjectType;
+    const inputField = type.getFields().filter;
+    expect(
+      resolveSchemaCoordinate(schema, 'SearchCriteria.filter'),
+    ).to.deep.equal({
+      kind: 'InputField',
+      type,
+      inputField,
+    });
+
+    expect(
+      resolveSchemaCoordinate(schema, 'SearchCriteria.unknown'),
+    ).to.deep.equal(undefined);
+  });
+
+  it('resolves a Enum Value', () => {
+    const type = schema.getType('SearchFilter') as GraphQLEnumType;
+    const enumValue = type.getValue('OPEN_NOW');
+    expect(
+      resolveSchemaCoordinate(schema, 'SearchFilter.OPEN_NOW'),
+    ).to.deep.equal({
+      kind: 'EnumValue',
+      type,
+      enumValue,
+    });
+
+    expect(
+      resolveSchemaCoordinate(schema, 'SearchFilter.UNKNOWN'),
+    ).to.deep.equal(undefined);
+  });
+
+  it('resolves a Field Argument', () => {
+    const type = schema.getType('Query') as GraphQLObjectType;
+    const field = type.getFields().searchBusiness;
+    const fieldArgument = field.args.find((arg) => arg.name === 'criteria');
+    expect(
+      resolveSchemaCoordinate(schema, 'Query.searchBusiness(criteria:)'),
+    ).to.deep.equal({
+      kind: 'FieldArgument',
+      type,
+      field,
+      fieldArgument,
+    });
+
+    expect(
+      resolveSchemaCoordinate(schema, 'Business.name(unknown:)'),
+    ).to.deep.equal(undefined);
+
+    expect(
+      resolveSchemaCoordinate(schema, 'Unknown.field(arg:)'),
+    ).to.deep.equal(undefined);
+
+    expect(
+      resolveSchemaCoordinate(schema, 'Business.unknown(arg:)'),
+    ).to.deep.equal(undefined);
+
+    expect(
+      resolveSchemaCoordinate(schema, 'SearchCriteria.name(arg:)'),
+    ).to.deep.equal(undefined);
+  });
+
+  it('resolves a Directive', () => {
+    expect(resolveSchemaCoordinate(schema, '@private')).to.deep.equal({
+      kind: 'Directive',
+      directive: schema.getDirective('private'),
+    });
+
+    expect(resolveSchemaCoordinate(schema, '@deprecated')).to.deep.equal({
+      kind: 'Directive',
+      directive: schema.getDirective('deprecated'),
+    });
+
+    expect(resolveSchemaCoordinate(schema, '@unknown')).to.deep.equal(
+      undefined,
+    );
+
+    expect(resolveSchemaCoordinate(schema, '@Business')).to.deep.equal(
+      undefined,
+    );
+  });
+
+  it('resolves a Directive Argument', () => {
+    const directive = schema.getDirective('private') as GraphQLDirective;
+    const directiveArgument = directive.args.find(
+      (arg) => arg.name === 'scope',
+    );
+    expect(resolveSchemaCoordinate(schema, '@private(scope:)')).to.deep.equal({
+      kind: 'DirectiveArgument',
+      directive,
+      directiveArgument,
+    });
+
+    expect(resolveSchemaCoordinate(schema, '@private(unknown:)')).to.deep.equal(
+      undefined,
+    );
+
+    expect(resolveSchemaCoordinate(schema, '@unknown(arg:)')).to.deep.equal(
+      undefined,
+    );
+  });
+});
diff --git a/src/utilities/index.ts b/src/utilities/index.ts
index 5b891cded1..470ff1ee29 100644
--- a/src/utilities/index.ts
+++ b/src/utilities/index.ts
@@ -127,3 +127,10 @@ export type {
 
 // Wrapper type that contains DocumentNode and types that can be deduced from it.
 export type { TypedQueryDocumentNode } from './typedQueryDocumentNode.js';
+
+// Schema coordinates
+export {
+  resolveSchemaCoordinate,
+  resolveASTSchemaCoordinate,
+} from './resolveSchemaCoordinate.js';
+export type { ResolvedSchemaElement } from './resolveSchemaCoordinate.js';
diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts
new file mode 100644
index 0000000000..9b9ce6f38b
--- /dev/null
+++ b/src/utilities/resolveSchemaCoordinate.ts
@@ -0,0 +1,190 @@
+import type { SchemaCoordinateNode } from '../language/ast.js';
+import { parseSchemaCoordinate } from '../language/parser.js';
+import type { Source } from '../language/source.js';
+
+import type {
+  GraphQLArgument,
+  GraphQLEnumValue,
+  GraphQLField,
+  GraphQLInputField,
+  GraphQLNamedType,
+} from '../type/definition.js';
+import {
+  isEnumType,
+  isInputObjectType,
+  isInterfaceType,
+  isObjectType,
+} from '../type/definition.js';
+import type { GraphQLDirective } from '../type/directives.js';
+import type { GraphQLSchema } from '../type/schema.js';
+
+/**
+ * A resolved schema element may be one of the following kinds:
+ */
+export type ResolvedSchemaElement =
+  | {
+      readonly kind: 'NamedType';
+      readonly type: GraphQLNamedType;
+    }
+  | {
+      readonly kind: 'Field';
+      readonly type: GraphQLNamedType;
+      readonly field: GraphQLField<unknown, unknown>;
+    }
+  | {
+      readonly kind: 'InputField';
+      readonly type: GraphQLNamedType;
+      readonly inputField: GraphQLInputField;
+    }
+  | {
+      readonly kind: 'EnumValue';
+      readonly type: GraphQLNamedType;
+      readonly enumValue: GraphQLEnumValue;
+    }
+  | {
+      readonly kind: 'FieldArgument';
+      readonly type: GraphQLNamedType;
+      readonly field: GraphQLField<unknown, unknown>;
+      readonly fieldArgument: GraphQLArgument;
+    }
+  | {
+      readonly kind: 'Directive';
+      readonly directive: GraphQLDirective;
+    }
+  | {
+      readonly kind: 'DirectiveArgument';
+      readonly directive: GraphQLDirective;
+      readonly directiveArgument: GraphQLArgument;
+    };
+
+/**
+ * A schema coordinate is resolved in the context of a GraphQL schema to
+ * uniquely identifies a schema element. It returns undefined if the schema
+ * coordinate does not resolve to a schema element.
+ *
+ * https://spec.graphql.org/draft/#sec-Schema-Coordinates.Semantics
+ */
+export function resolveSchemaCoordinate(
+  schema: GraphQLSchema,
+  schemaCoordinate: string | Source,
+): ResolvedSchemaElement | undefined {
+  return resolveASTSchemaCoordinate(
+    schema,
+    parseSchemaCoordinate(schemaCoordinate),
+  );
+}
+
+/**
+ * Resolves schema coordinate from a parsed SchemaCoordinate node.
+ */
+export function resolveASTSchemaCoordinate(
+  schema: GraphQLSchema,
+  schemaCoordinate: SchemaCoordinateNode,
+): ResolvedSchemaElement | undefined {
+  const { ofDirective, name, memberName, argumentName } = schemaCoordinate;
+  if (ofDirective) {
+    // SchemaCoordinate :
+    //   - @ Name
+    //   - @ Name ( Name : )
+    // Let {directiveName} be the value of the first {Name}.
+    // Let {directive} be the directive in the {schema} named {directiveName}.
+    const directive = schema.getDirective(name.value);
+    if (!argumentName) {
+      // SchemaCoordinate : @ Name
+      // Return the directive in the {schema} named {directiveName}.
+      if (!directive) {
+        return;
+      }
+      return { kind: 'Directive', directive };
+    }
+
+    // SchemaCoordinate : @ Name ( Name : )
+    // Assert {directive} must exist.
+    if (!directive) {
+      return;
+    }
+    // Let {directiveArgumentName} be the value of the second {Name}.
+    // Return the argument of {directive} named {directiveArgumentName}.
+    const directiveArgument = directive.args.find(
+      (arg) => arg.name === argumentName.value,
+    );
+    if (!directiveArgument) {
+      return;
+    }
+    return { kind: 'DirectiveArgument', directive, directiveArgument };
+  }
+
+  // SchemaCoordinate :
+  //   - Name
+  //   - Name . Name
+  //   - Name . Name ( Name : )
+  // Let {typeName} be the value of the first {Name}.
+  // Let {type} be the type in the {schema} named {typeName}.
+  const type = schema.getType(name.value);
+  if (!memberName) {
+    // SchemaCoordinate : Name
+    // Return the type in the {schema} named {typeName}.
+    if (!type) {
+      return;
+    }
+    return { kind: 'NamedType', type };
+  }
+
+  if (!argumentName) {
+    // SchemaCoordinate : Name . Name
+    // If {type} is an Enum type:
+    if (isEnumType(type)) {
+      // Let {enumValueName} be the value of the second {Name}.
+      // Return the enum value of {type} named {enumValueName}.
+      const enumValue = type.getValue(memberName.value);
+      if (!enumValue) {
+        return;
+      }
+      return { kind: 'EnumValue', type, enumValue };
+    }
+    // Otherwise if {type} is an Input Object type:
+    if (isInputObjectType(type)) {
+      // Let {inputFieldName} be the value of the second {Name}.
+      // Return the input field of {type} named {inputFieldName}.
+      const inputField = type.getFields()[memberName.value];
+      if (inputField == null) {
+        return;
+      }
+      return { kind: 'InputField', type, inputField };
+    }
+    // Otherwise:
+    // Assert {type} must be an Object or Interface type.
+    if (!isObjectType(type) && !isInterfaceType(type)) {
+      return;
+    }
+    // Let {fieldName} be the value of the second {Name}.
+    // Return the field of {type} named {fieldName}.
+    const field = type.getFields()[memberName.value];
+    if (field == null) {
+      return;
+    }
+    return { kind: 'Field', type, field };
+  }
+
+  // SchemaCoordinate : Name . Name ( Name : )
+  // Assert {type} must be an Object or Interface type.
+  if (!isObjectType(type) && !isInterfaceType(type)) {
+    return;
+  }
+  // Let {fieldName} be the value of the second {Name}.
+  // Let {field} be the field of {type} named {fieldName}.
+  const field = type.getFields()[memberName.value];
+  // Assert {field} must exist.
+  if (field == null) {
+    return;
+  }
+  // Let {fieldArgumentName} be the value of the third {Name}.
+  // Return the argument of {field} named {fieldArgumentName}.
+  const fieldArgument = field.args.find(
+    (arg) => arg.name === argumentName.value,
+  );
+  if (!fieldArgument) {
+    return;
+  }
+  return { kind: 'FieldArgument', type, field, fieldArgument };
+}